diff --git a/README.md b/README.md index 53bd595e..45567839 100644 --- a/README.md +++ b/README.md @@ -274,11 +274,11 @@ dbswitch.target.writer-engine-insert=true ### 2、基于conf/application.yml配置的dbswitch-admin模块启动的WEB使用方式 -#### (1)、准备一个MySQL(建议版本为: 5.7+ )或PostgreSQL(建议版本:11.7+ )的数据库 +#### (1)、准备一个MySQL(建议版本为: 5.7+ )或PostgreSQL(建议版本:11.7+ )或者OpenGauss(建议版本:5.0+ )的数据库 -> dbswitch-admin模块后端同时支持MySQL、PostgreSQL作为配置数据库。 +> dbswitch-admin模块后端同时支持MySQL、PostgreSQL、OpenGauss作为配置数据库。 -#### (2)、配置conf/application.yml(MySQL可参考application_sample_mysql.yml配置,PostgreSQL可参考application_sample_postgresql.yml配置) +#### (2)、配置conf/application.yml(MySQL可参考application_sample_mysql.yml配置,PostgreSQL/OpenGauss可参考application_sample_postgresql.yml配置) MySQL的application.yml配置内容示例如下: diff --git a/SUPPORTED_PRODUCTS.md b/SUPPORTED_PRODUCTS.md index c7fd0d39..cad30b25 100644 --- a/SUPPORTED_PRODUCTS.md +++ b/SUPPORTED_PRODUCTS.md @@ -2,14 +2,14 @@ 支持的数据库产品及其JDBC驱动连接示例如下: -**MySQL/MariaDB/StarRocks数据库** +**MySQL数据库** ``` jdbc连接地址:jdbc:mysql://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true jdbc驱动名称: com.mysql.jdbc.Driver ``` -与: +**MariaDB数据库** ``` jdbc连接地址:jdbc:mariadb://172.17.2.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&rewriteBatchedStatements=true&useCompression=true @@ -23,7 +23,7 @@ jdbc连接地址:jdbc:oracle:thin:@172.17.2.10:1521:ORCL 或 jdbc:oracle:th jdbc驱动名称:oracle.jdbc.driver.OracleDriver ``` -**SQL Server(>=2005)数据库** +**SQLServer(>=2005)数据库** ``` jdbc连接地址:jdbc:sqlserver://172.17.2.10:1433;DatabaseName=test @@ -38,7 +38,14 @@ jdbc驱动名称:com.sybase.jdbc4.jdbc.SybDriver ``` > JDBC连接Sybase数据库使用中文时只能使用CP936这个字符集 -**PostgreSQL/Greenplum数据库** +**PostgreSQL数据库** + +``` +jdbc连接地址:jdbc:postgresql://172.17.2.10:5432/test +jdbc驱动名称:org.postgresql.Driver +``` + +**Greenplum数据库** ``` jdbc连接地址:jdbc:postgresql://172.17.2.10:5432/test @@ -59,7 +66,7 @@ jdbc连接地址:jdbc:dm://172.17.2.10:5236 jdbc驱动名称:dm.jdbc.driver.DmDriver ``` -**人大金仓Kingbase8数据库** +**金仓Kingbase8数据库** ``` jdbc连接地址:jdbc:kingbase8://172.17.2.10:54321/MYTEST @@ -131,7 +138,7 @@ jdbc连接地址:jdbc:clickhouse://172.17.2.10:8123/default jdbc驱动名称:com.clickhouse.jdbc.ClickHouseDriver ``` -**SQLite数据库** +**SQLite3数据库** ``` jdbc连接地址:jdbc:sqlite:/tmp/test.db 或者 jdbc:sqlite::resource:http://172.17.2.12:8080/test.db @@ -164,7 +171,7 @@ jdbc驱动名称:com.gitee.jdbc.mongodb.JdbcDriver > 项目地址:https://gitee.com/inrgihc/jdbc-mongodb-driver -**ElasticSearch数据库** +**ElasticSearch(7.x版本)数据库** ``` jdbc连接地址:jdbc:jest://172.17.2.12:9200?useHttps=false diff --git a/dbswitch-admin/pom.xml b/dbswitch-admin/pom.xml index 2e685712..5841e9ff 100644 --- a/dbswitch-admin/pom.xml +++ b/dbswitch-admin/pom.xml @@ -30,6 +30,12 @@ ${project.version} + + com.gitee.dbswitch + flyway-core + ${project.version} + + org.springframework.boot spring-boot-starter-web @@ -65,11 +71,6 @@ spring-context-support - - org.flywaydb - flyway-core - - org.projectlombok lombok diff --git a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/controller/converter/AssignmentDetailConverter.java b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/controller/converter/AssignmentDetailConverter.java index 19418016..527cf10c 100644 --- a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/controller/converter/AssignmentDetailConverter.java +++ b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/controller/converter/AssignmentDetailConverter.java @@ -38,7 +38,7 @@ public class AssignmentDetailConverter extends config.setSourceConnectionName(srcConn.getName()); config.setSourceSchema(taskConfig.getSourceSchema()); config.setTableType(taskConfig.getTableType()); - config.setIncludeOrExclude(taskConfig.getExcluded() + config.setIncludeOrExclude(taskConfig.getExcludedFlag() ? IncludeExcludeEnum.EXCLUDE : IncludeExcludeEnum.INCLUDE); config.setSourceTables(taskConfig.getSourceTables()); diff --git a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/entity/AssignmentConfigEntity.java b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/entity/AssignmentConfigEntity.java index b9330654..b897d6d3 100644 --- a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/entity/AssignmentConfigEntity.java +++ b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/entity/AssignmentConfigEntity.java @@ -53,8 +53,8 @@ public class AssignmentConfigEntity { @TableField(value = "source_tables", typeHandler = ListTypeHandler.class) private List sourceTables; - @TableField("excluded") - private Boolean excluded; + @TableField("excluded_flag") + private Boolean excludedFlag; @TableField("target_connection_id") private Long targetConnectionId; diff --git a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentBaseRequest.java b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentBaseRequest.java index d424200b..beb1e615 100644 --- a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentBaseRequest.java +++ b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentBaseRequest.java @@ -54,7 +54,7 @@ public class AssigmentBaseRequest { assignmentConfigEntity.setSourceSchema(config.getSourceSchema()); assignmentConfigEntity.setTableType(config.getTableType()); assignmentConfigEntity.setSourceTables(config.getSourceTables()); - assignmentConfigEntity.setExcluded( + assignmentConfigEntity.setExcludedFlag( config.getIncludeOrExclude() == IncludeExcludeEnum.EXCLUDE ); assignmentConfigEntity.setTargetConnectionId(config.getTargetConnectionId()); diff --git a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentCreateRequest.java b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentCreateRequest.java index d537799b..6d423b87 100644 --- a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentCreateRequest.java +++ b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/model/request/AssigmentCreateRequest.java @@ -54,7 +54,7 @@ public class AssigmentCreateRequest extends AssigmentBaseRequest { AssignmentConfigEntity assignmentConfigEntity = toAssignmentConfig(assignmentId, config); assignmentConfigEntity.setFirstFlag(Boolean.TRUE); - if (!assignmentConfigEntity.getExcluded() + if (!assignmentConfigEntity.getExcludedFlag() && !CollectionUtils.isEmpty(assignmentConfigEntity.getSourceTables())) { for (String tableName : assignmentConfigEntity.getSourceTables()) { String targetTableName = PatterNameUtils.getFinalName(tableName, diff --git a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/service/AssignmentService.java b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/service/AssignmentService.java index faf4da72..7a0e3da7 100644 --- a/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/service/AssignmentService.java +++ b/dbswitch-admin/src/main/java/com/gitee/dbswitch/admin/service/AssignmentService.java @@ -272,7 +272,7 @@ public class AssignmentService { sourceDataSourceProperties.setPassword(sourceDatabaseConnectionEntity.getPassword()); String sourceSchema = assignmentConfigEntity.getSourceSchema(); - if (assignmentConfigEntity.getExcluded()) { + if (assignmentConfigEntity.getExcludedFlag()) { if (CollectionUtils.isEmpty(assignmentConfigEntity.getSourceTables())) { sourceDataSourceProperties.setSourceExcludes(""); } else { diff --git a/dbswitch-admin/src/main/resources/db/migration/V1_0_14__system-ddl.sql b/dbswitch-admin/src/main/resources/db/migration/V1_0_14__system-ddl.sql new file mode 100644 index 00000000..f9c79dc7 --- /dev/null +++ b/dbswitch-admin/src/main/resources/db/migration/V1_0_14__system-ddl.sql @@ -0,0 +1,2 @@ +ALTER TABLE `DBSWITCH_ASSIGNMENT_CONFIG` +CHANGE COLUMN `excluded` `excluded_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否排除(0:否 1:是)' AFTER `source_tables`; diff --git a/dbswitch-admin/src/main/resources/db/postgres/V1_0_1__system-ddl.sql b/dbswitch-admin/src/main/resources/db/postgres/V1_0_1__system-ddl.sql index c8463f04..f00256d2 100644 --- a/dbswitch-admin/src/main/resources/db/postgres/V1_0_1__system-ddl.sql +++ b/dbswitch-admin/src/main/resources/db/postgres/V1_0_1__system-ddl.sql @@ -123,7 +123,7 @@ CREATE TABLE IF NOT EXISTS DBSWITCH_ASSIGNMENT_CONFIG ( "source_schema" varchar(1024) not null, "table_type" varchar(32) not null default 'TABLE', "source_tables" text , - "excluded" boolean not null default false, + "excluded_flag" boolean not null default false, "target_connection_id" int8 not null, "table_name_case" varchar(32) not null default 'NONE', "column_name_case" varchar(32) not null default 'NONE', @@ -153,7 +153,7 @@ COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."source_connection_id" IS '来源 COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."source_schema" IS '来源端的schema'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."table_type" IS '表类型:TABLE;VIEW'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."source_tables" IS '来源端的table列表'; -COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."excluded" IS '是否排除(0:否 1:是)'; +COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."excluded_flag" IS '是否排除(0:否 1:是)'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."target_connection_id" IS '目的端连接ID'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."target_schema" IS '目的端的schema(一个)'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_CONFIG."table_name_case" IS '表名大小写转换策略'; @@ -185,7 +185,6 @@ CREATE TABLE IF NOT EXISTS DBSWITCH_ASSIGNMENT_JOB ( primary key ("id"), foreign key ("assignment_id") references DBSWITCH_ASSIGNMENT_TASK ("id") on delete cascade on update cascade ); - COMMENT ON TABLE DBSWITCH_ASSIGNMENT_JOB IS 'JOB日志表'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."id" IS '主键'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."assignment_id" IS '任务ID'; @@ -197,3 +196,17 @@ COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."status" IS '执行状态:0-未执行; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."error_log" IS '异常日志'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."create_time" IS '创建时间'; COMMENT ON COLUMN DBSWITCH_ASSIGNMENT_JOB."update_time" IS '修改时间'; + +CREATE TABLE IF NOT EXISTS DBSWITCH_JOB_LOGBACK ( + "id" bigserial not null, + "uuid" varchar(128) not null default '', + "content" text, + "create_time" timestamp(6) not null default (CURRENT_TIMESTAMP(0))::timestamp(0) without time zone, + PRIMARY KEY ("id") +); +CREATE INDEX DBSWITCH_JOB_LOGBACK_UUID_IDX ON DBSWITCH_JOB_LOGBACK("uuid"); +COMMENT ON TABLE DBSWITCH_JOB_LOGBACK IS 'JOB执行日志'; +COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."id" IS '主键'; +COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."uuid" IS 'job id'; +COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."content" IS '日志内容'; +COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."create_time" IS '创建时间'; diff --git a/dbswitch-admin/src/main/resources/db/postgres/V1_0_3__system-ddl.sql b/dbswitch-admin/src/main/resources/db/postgres/V1_0_3__system-ddl.sql deleted file mode 100644 index 9b2dc3fa..00000000 --- a/dbswitch-admin/src/main/resources/db/postgres/V1_0_3__system-ddl.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS DBSWITCH_JOB_LOGBACK ( - "id" bigserial not null, - "uuid" varchar(128) not null default '', - "content" text, - "create_time" timestamp(6) not null default (CURRENT_TIMESTAMP(0))::timestamp(0) without time zone, - PRIMARY KEY ("id") -); - -CREATE INDEX DBSWITCH_JOB_LOGBACK_UUID_IDX ON DBSWITCH_JOB_LOGBACK("uuid"); -COMMENT ON TABLE DBSWITCH_JOB_LOGBACK IS 'JOB执行日志'; -COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."id" IS '主键'; -COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."uuid" IS 'job id'; -COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."content" IS '日志内容'; -COMMENT ON COLUMN DBSWITCH_JOB_LOGBACK."create_time" IS '创建时间'; diff --git a/flyway-core/REAME.md b/flyway-core/REAME.md new file mode 100644 index 00000000..56342b03 --- /dev/null +++ b/flyway-core/REAME.md @@ -0,0 +1,8 @@ +# 关于flyway-core的说明 + +flyway开源项目地址:https://github.com/flyway/flyway + +本模块源代码来自于flyway项目的开源的flyway-core模块代码(版本为6.4.4),基于如下文章对其对openGauss数据库进行适配: + +> https://bbs.huaweicloud.com/blogs/411017 + diff --git a/flyway-core/pom.xml b/flyway-core/pom.xml new file mode 100644 index 00000000..2347539c --- /dev/null +++ b/flyway-core/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + com.gitee.dbswitch + dbswitch-parent + 1.9.9 + + flyway-core + jar + + + org.slf4j + slf4j-api + true + + + commons-logging + commons-logging + true + + + org.jboss + jboss-vfs + true + + + org.osgi + org.osgi.core + true + provided + + + com.google.android + android + true + + + org.postgresql + postgresql + true + + + + + + + + + src/main/resources + true + + + + + maven-resources-plugin + + + copy-license + + copy-resources + + generate-resources + + + + .. + + LICENSE.txt + README.txt + + + + ${project.build.outputDirectory}/META-INF + + + + + + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + + + org.flywaydb.core + org.flywaydb.core + + org.flywaydb.core;version=${project.version}, + org.flywaydb.core.api.*;version=${project.version} + + + android.content;version="[4.0.1.2,9)";resolution:=optional, + android.content.pm;version="[4.0.1.2,9)";resolution:=optional, + android.content.res;version="[4.0.1.2,9)";resolution:=optional, + android.util;version="[4.0.1.2,9)";resolution:=optional, + dalvik.system;version="[4.0.1.2,9)";resolution:=optional, + javax.sql, + org.apache.commons.logging;version="[1.1,2)";resolution:=optional, + org.jboss.vfs;version="[3.1.0,4)";resolution:=optional, + org.postgresql.copy;version="[9.3.1102,100.0)";resolution:=optional, + org.postgresql.core;version="[9.3.1102,100.0)";resolution:=optional, + org.osgi.framework;version="1.3.0";resolution:=mandatory, + org.slf4j;version="[1.6,2)";resolution:=optional, + org.springframework.*;version="[2.5,6.0)";resolution:=optional + + + + + + bundle-manifest + process-classes + + manifest + + + + + + maven-javadoc-plugin + + + **/core/Flyway.java + **/core/api/**/*.java + + + + + + + + + + maven-javadoc-plugin + 3.0.1 + + + **/core/Flyway.java + **/core/api/**/*.java + + + + + + + + \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/Flyway.java b/flyway-core/src/main/java/org/flywaydb/core/Flyway.java new file mode 100644 index 00000000..d8118b44 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/Flyway.java @@ -0,0 +1,694 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.configuration.ClassicConfiguration; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.internal.callback.*; +import org.flywaydb.core.internal.clazz.ClassProvider; +import org.flywaydb.core.internal.clazz.NoopClassProvider; +import org.flywaydb.core.internal.command.*; +import org.flywaydb.core.internal.configuration.ConfigurationValidator; +import org.flywaydb.core.internal.database.DatabaseFactory; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.license.VersionPrinter; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.core.internal.resolver.CompositeMigrationResolver; +import org.flywaydb.core.internal.resource.NoopResourceProvider; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.resource.StringResource; +import org.flywaydb.core.internal.resource.ResourceNameValidator; +import org.flywaydb.core.internal.scanner.LocationScannerCache; +import org.flywaydb.core.internal.scanner.ResourceNameCache; +import org.flywaydb.core.internal.scanner.Scanner; +import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.schemahistory.SchemaHistoryFactory; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; +import org.flywaydb.core.internal.util.IOUtils; +import org.flywaydb.core.internal.util.Pair; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.util.*; + +/** + * This is the centre point of Flyway, and for most users, the only class they will ever have to deal with. + *

+ * It is THE public API from which all important Flyway functions such as clean, validate and migrate can be called. + *

+ *

To get started all you need to do is create a configured Flyway object and then invoke its principal methods.

+ *
+ * Flyway flyway = Flyway.configure().dataSource(url, user, password).load();
+ * flyway.migrate();
+ * 
+ * Note that a configured Flyway object is immutable. If you change the configuration you will end up creating a new Flyway + * object. + *

+ */ +public class Flyway { + private static final Log LOG = LogFactory.getLog(Flyway.class); + + private final ClassicConfiguration configuration; + + /** + * Whether the database connection info has already been printed in the logs. + */ + private boolean dbConnectionInfoPrinted; + + /** + * Designed so we can fail fast if the configuration is invalid + */ + private ConfigurationValidator configurationValidator = new ConfigurationValidator(); + + /** + * Designed so we can fail fast if the SQL file resources are invalid + */ + private ResourceNameValidator resourceNameValidator = new ResourceNameValidator(); + + /** + * This is your starting point. This creates a configuration which can be customized to your needs before being + * loaded into a new Flyway instance using the load() method. + *

In its simplest form, this is how you configure Flyway with all defaults to get started:

+ *
Flyway flyway = Flyway.configure().dataSource(url, user, password).load();
+ *

After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway + * functionality such as migrate() or clean().

+ * + * @return A new configuration from which Flyway can be loaded. + */ + public static FluentConfiguration configure() { + return new FluentConfiguration(); + } + + /** + * This is your starting point. This creates a configuration which can be customized to your needs before being + * loaded into a new Flyway instance using the load() method. + *

In its simplest form, this is how you configure Flyway with all defaults to get started:

+ *
Flyway flyway = Flyway.configure().dataSource(url, user, password).load();
+ *

After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway + * functionality such as migrate() or clean().

+ * + * @param classLoader The class loader to use when loading classes and resources. + * @return A new configuration from which Flyway can be loaded. + */ + public static FluentConfiguration configure(ClassLoader classLoader) { + return new FluentConfiguration(classLoader); + } + + /** + * Creates a new instance of Flyway with this configuration. In general the Flyway.configure() factory method should + * be preferred over this constructor, unless you need to create or reuse separate Configuration objects. + * + * @param configuration The configuration to use. + */ + public Flyway(Configuration configuration) { + this.configuration = new ClassicConfiguration(configuration); + } + + /** + * @return The configuration that Flyway is using. + */ + public Configuration getConfiguration() { + return new ClassicConfiguration(configuration); + } + + /** + * Used to cache resource names for classpath scanning between commands + */ + private ResourceNameCache resourceNameCache = new ResourceNameCache(); + + /** + * Used to cache LocationScanners between commands + */ + private final LocationScannerCache locationScannerCache = new LocationScannerCache(); + + /** + *

Starts the database migration. All pending migrations will be applied in order. + * Calling migrate on an up-to-date database has no effect.

+ * migrate + * + * @return The number of successfully applied migrations. + * @throws FlywayException when the migration failed. + */ + public int migrate() throws FlywayException { + return execute(new Command() { + public Integer execute(MigrationResolver migrationResolver, + SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + if (configuration.isValidateOnMigrate()) { + doValidate(database, migrationResolver, schemaHistory, schemas, callbackExecutor, + true // Always ignore pending migrations when validating before migrating + ); + } + + if (!schemaHistory.exists()) { + List nonEmptySchemas = new ArrayList<>(); + for (Schema schema : schemas) { + if (schema.exists() && !schema.empty()) { + nonEmptySchemas.add(schema); + } + } + + if (!nonEmptySchemas.isEmpty()) { + if (configuration.isBaselineOnMigrate()) { + doBaseline(schemaHistory, callbackExecutor); + } else { + // Second check for MySQL which is sometimes flaky otherwise + if (!schemaHistory.exists()) { + throw new FlywayException("Found non-empty schema(s) " + + StringUtils.collectionToCommaDelimitedString(nonEmptySchemas) + + " but no schema history table. Use baseline()" + + " or set baselineOnMigrate to true to initialize the schema history table."); + } + } + } else { + new DbSchemas(database, schemas, schemaHistory).create(false); + schemaHistory.create(false); + } + } + + return new DbMigrate(database, schemaHistory, schemas[0], migrationResolver, configuration, + callbackExecutor).migrate(); + } + }, true); + } + + private void doBaseline(SchemaHistory schemaHistory, CallbackExecutor callbackExecutor) { + new DbBaseline(schemaHistory, configuration.getBaselineVersion(), configuration.getBaselineDescription(), + callbackExecutor).baseline(); + } + + /** + *

Undoes the most recently applied versioned migration. If target is specified, Flyway will attempt to undo + * versioned migrations in the order they were applied until it hits one with a version below the target. If there + * is no versioned migration to undo, calling undo has no effect.

+ *

Flyway Pro and Flyway Enterprise only

+ * undo + * + * @return The number of successfully undone migrations. + * @throws FlywayException when the undo failed. + */ + public int undo() throws FlywayException { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("undo"); + + + + + + + + + + + + + } + + /** + *

Validate applied migrations against resolved ones (on the filesystem or classpath) + * to detect accidental changes that may prevent the schema(s) from being recreated exactly.

+ *

Validation fails if

+ *
    + *
  • differences in migration names, types or checksums are found
  • + *
  • versions have been applied that aren't resolved locally anymore
  • + *
  • versions have been resolved that haven't been applied yet
  • + *
+ * + * validate + * + * @throws FlywayException when the validation failed. + */ + public void validate() throws FlywayException { + execute(new Command() { + public Void execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, Database database, + Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + doValidate(database, migrationResolver, schemaHistory, schemas, callbackExecutor, + configuration.isIgnorePendingMigrations()); + return null; + } + }, true); + } + + /** + * Performs the actual validation. All set up must have taken place beforehand. + * + * @param database The database-specific support. + * @param migrationResolver The migration resolver; + * @param schemaHistory The schema history table. + * @param schemas The schemas managed by Flyway. + * @param callbackExecutor The callback executor. + * @param ignorePending Whether to ignore pending migrations. + */ + private void doValidate(Database database, MigrationResolver migrationResolver, SchemaHistory schemaHistory, + Schema[] schemas, CallbackExecutor callbackExecutor, boolean ignorePending) { + String validationError = + new DbValidate(database, schemaHistory, schemas[0], migrationResolver, + configuration, ignorePending, callbackExecutor).validate(); + + if (validationError != null) { + if (configuration.isCleanOnValidationError()) { + doClean(database, schemaHistory, schemas, callbackExecutor); + } else { + throw new FlywayException("Validate failed: " + validationError); + } + } + } + + private void doClean(Database database, SchemaHistory schemaHistory, Schema[] schemas, CallbackExecutor callbackExecutor) { + new DbClean(database, schemaHistory, schemas, callbackExecutor, configuration.isCleanDisabled()).clean(); + } + + /** + *

Drops all objects (tables, views, procedures, triggers, ...) in the configured schemas. + * The schemas are cleaned in the order specified by the {@code schemas} property.

+ * clean + * + * @throws FlywayException when the clean fails. + */ + public void clean() { + execute(new Command() { + public Void execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, Database database, + Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + doClean(database, schemaHistory, schemas, callbackExecutor); + return null; + } + }, false); + } + + /** + *

Retrieves the complete information about all the migrations including applied, pending and current migrations with + * details and status.

+ * info + * + * @return All migrations sorted by version, oldest first. + * @throws FlywayException when the info retrieval failed. + */ + public MigrationInfoService info() { + return execute(new Command() { + public MigrationInfoService execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, + final Database database, final Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + return new DbInfo(migrationResolver, schemaHistory, configuration, callbackExecutor).info(); + } + }, true); + } + + /** + *

Baselines an existing database, excluding all migrations up to and including baselineVersion.

+ * + * baseline + * + * @throws FlywayException when the schema baselining failed. + */ + public void baseline() throws FlywayException { + execute(new Command() { + public Void execute(MigrationResolver migrationResolver, + SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + new DbSchemas(database, schemas, schemaHistory).create(true); + doBaseline(schemaHistory, callbackExecutor); + return null; + } + }, false); + } + + /** + * Repairs the Flyway schema history table. This will perform the following actions: + *
    + *
  • Remove any failed migrations on databases without DDL transactions (User objects left behind must still be cleaned up manually)
  • + *
  • Realign the checksums, descriptions and types of the applied migrations with the ones of the available migrations
  • + *
+ * repair + * + * @throws FlywayException when the schema history table repair failed. + */ + public void repair() throws FlywayException { + execute(new Command() { + public Void execute(MigrationResolver migrationResolver, + SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor + + + + ) { + new DbRepair(database, migrationResolver, schemaHistory, callbackExecutor, configuration).repair(); + return null; + } + }, true); + } + + /** + * Creates the MigrationResolver. + * + * @param resourceProvider The resource provider. + * @param classProvider The class provider. + * @param sqlScriptFactory The SQL statement builder factory. + * @param parsingContext The parsing context. + * @return A new, fully configured, MigrationResolver instance. + */ + private MigrationResolver createMigrationResolver(ResourceProvider resourceProvider, + ClassProvider classProvider, + SqlScriptExecutorFactory sqlScriptExecutorFactory, + SqlScriptFactory sqlScriptFactory, + ParsingContext parsingContext) { + return new CompositeMigrationResolver(resourceProvider, classProvider, configuration, + sqlScriptExecutorFactory, sqlScriptFactory, parsingContext, configuration.getResolvers()); + } + + /** + * Executes this command with proper resource handling and cleanup. + * + * @param command The command to execute. + * @param The type of the result. + * @return The result of the command. + */ + /*private -> testing*/ T execute(Command command, boolean scannerRequired) { + T result; + + VersionPrinter.printVersion( + + + + ); + + configurationValidator.validate(configuration); + + + + + + + + + + + + + final ResourceProvider resourceProvider; + ClassProvider classProvider; + if (!scannerRequired && configuration.isSkipDefaultResolvers() && configuration.isSkipDefaultCallbacks()) { + resourceProvider = NoopResourceProvider.INSTANCE; + //noinspection unchecked + classProvider = NoopClassProvider.INSTANCE; + } else { + Scanner scanner = new Scanner<>( + JavaMigration.class, + Arrays.asList(configuration.getLocations()), + configuration.getClassLoader(), + configuration.getEncoding() + + + + , resourceNameCache + , locationScannerCache + ); + resourceProvider = scanner; + classProvider = scanner; + } + + if (configuration.isValidateMigrationNaming()) { + resourceNameValidator.validateSQLMigrationNaming(resourceProvider, configuration); + } + + JdbcConnectionFactory jdbcConnectionFactory = new JdbcConnectionFactory(configuration.getDataSource(), + configuration.getConnectRetries() + + + + + ); + + final ParsingContext parsingContext = new ParsingContext(); + final SqlScriptFactory sqlScriptFactory = + DatabaseFactory.createSqlScriptFactory(jdbcConnectionFactory, configuration, parsingContext); + + final SqlScriptExecutorFactory noCallbackSqlScriptExecutorFactory = DatabaseFactory.createSqlScriptExecutorFactory( + jdbcConnectionFactory + + + + + ); + + jdbcConnectionFactory.setConnectionInitializer(new JdbcConnectionFactory.ConnectionInitializer() { + @Override + public void initialize(JdbcConnectionFactory jdbcConnectionFactory, Connection connection) { + if (configuration.getInitSql() == null) { + return; + } + StringResource resource = new StringResource(configuration.getInitSql()); + + SqlScript sqlScript = sqlScriptFactory.createSqlScript(resource, true, resourceProvider); + noCallbackSqlScriptExecutorFactory.createSqlScriptExecutor(connection + + + + ).execute(sqlScript); + } + }); + + Database database = null; + try { + database = DatabaseFactory.createDatabase(configuration, !dbConnectionInfoPrinted, jdbcConnectionFactory + + + + ); + + dbConnectionInfoPrinted = true; + LOG.debug("DDL Transactions Supported: " + database.supportsDdlTransactions()); + + Pair> schemas = prepareSchemas(database); + Schema defaultSchema = schemas.getLeft(); + + + + + + + + parsingContext.populate(database, configuration); + + database.ensureSupported(); + + DefaultCallbackExecutor callbackExecutor = new DefaultCallbackExecutor(configuration, database, defaultSchema, + prepareCallbacks(database, resourceProvider, jdbcConnectionFactory, sqlScriptFactory + + + + )); + + SqlScriptExecutorFactory sqlScriptExecutorFactory = DatabaseFactory.createSqlScriptExecutorFactory(jdbcConnectionFactory + + + + + ); + + result = command.execute( + createMigrationResolver(resourceProvider, classProvider, sqlScriptExecutorFactory, sqlScriptFactory, parsingContext), + SchemaHistoryFactory.getSchemaHistory(configuration, noCallbackSqlScriptExecutorFactory, sqlScriptFactory, + database, defaultSchema + + + + ), + database, + schemas.getRight().toArray(new Schema[0]), + callbackExecutor + + + + ); + } finally { + IOUtils.close(database); + + + + showMemoryUsage(); + } + return result; + } + + private void showMemoryUsage() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory(); + long total = runtime.totalMemory(); + long used = total - free; + + long totalMB = total / (1024 * 1024); + long usedMB = used / (1024 * 1024); + LOG.debug("Memory usage: " + usedMB + " of " + totalMB + "M"); + } + + private Pair> prepareSchemas(Database database) { + String defaultSchemaName = configuration.getDefaultSchema(); + String[] schemaNames = configuration.getSchemas(); + + if (!isDefaultSchemaValid(defaultSchemaName, schemaNames)) { + throw new FlywayException("The defaultSchema property is specified but is not a member of the schemas property"); + } + + LOG.debug("Schemas: " + StringUtils.arrayToCommaDelimitedString(schemaNames)); + LOG.debug("Default schema: " + defaultSchemaName); + + List schemas = new ArrayList<>(); + + if (schemaNames.length == 0) { + Schema currentSchema = database.getMainConnection().getCurrentSchema(); + if (currentSchema == null) { + throw new FlywayException("Unable to determine schema for the schema history table." + + " Set a default schema for the connection or specify one using the defaultSchema property!"); + } + schemas.add(currentSchema); + } else { + for (String schemaName : schemaNames) { + schemas.add(database.getMainConnection().getSchema(schemaName)); + } + } + + if (defaultSchemaName == null && schemaNames.length > 0) { + defaultSchemaName = schemaNames[0]; + } + + Schema defaultSchema = (defaultSchemaName != null) + ? database.getMainConnection().getSchema(defaultSchemaName) + : database.getMainConnection().getCurrentSchema(); + + return Pair.of(defaultSchema, schemas); + } + + private boolean isDefaultSchemaValid(String defaultSchema, String[] schemas) { + // No default schema specified + if (defaultSchema == null) { + return true; + } + // Default schema is one of those Flyway is managing + for (String schema : schemas) { + if (defaultSchema.equals(schema)) { + return true; + } + } + return false; + } + + private List prepareCallbacks(Database database, ResourceProvider resourceProvider, + JdbcConnectionFactory jdbcConnectionFactory, + SqlScriptFactory sqlScriptFactory + + + + + ) { + List effectiveCallbacks = new ArrayList<>(); + + + + + + + + + + + + + + + + + + + effectiveCallbacks.addAll(Arrays.asList(configuration.getCallbacks())); + + if (!configuration.isSkipDefaultCallbacks()) { + SqlScriptExecutorFactory sqlScriptExecutorFactory = + DatabaseFactory.createSqlScriptExecutorFactory(jdbcConnectionFactory + + + + + ); + + effectiveCallbacks.addAll( + new SqlScriptCallbackFactory( + resourceProvider, + sqlScriptExecutorFactory, + sqlScriptFactory, + configuration + ).getCallbacks()); + } + + + + + + return effectiveCallbacks; + } + + /** + * A Flyway command that can be executed. + * + * @param The result type of the command. + */ + /*private -> testing*/ interface Command { + /** + * Execute the operation. + * + * @param migrationResolver The migration resolver to use. + * @param schemaHistory The schema history table. + * @param database The database-specific support for these connections. + * @param schemas The schemas managed by Flyway. + * @param callbackExecutor The callback executor. + * @return The result of the operation. + */ + T execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, + Database database, Schema[] schemas, CallbackExecutor callbackExecutor + + + + ); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/ErrorCode.java b/flyway-core/src/main/java/org/flywaydb/core/api/ErrorCode.java new file mode 100644 index 00000000..035396c3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/ErrorCode.java @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +public enum ErrorCode { + FAULT, + ERROR, + JDBC_DRIVER, + DB_CONNECTION, + DUPLICATE_VERSIONED_MIGRATION, + DUPLICATE_REPEATABLE_MIGRATION, + DUPLICATE_UNDO_MIGRATION, + CONFIGURATION; +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/FlywayException.java b/flyway-core/src/main/java/org/flywaydb/core/api/FlywayException.java new file mode 100644 index 00000000..1d53a8b1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/FlywayException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +/** + * Exception thrown when Flyway encounters a problem. + */ +public class FlywayException extends RuntimeException { + + private ErrorCode errorCode = ErrorCode.ERROR; + + /** + * Creates a new FlywayException with this message, cause, and error code. + * + * @param message The exception message. + * @param cause The exception cause. + * @param errorCode The error code. + */ + public FlywayException(String message, Throwable cause, ErrorCode errorCode) { + super(message, cause); + this.errorCode = errorCode; + } + + /** + * Creates a new FlywayException with this message and error code + * + * @param message The exception message. + * @param errorCode The error code. + */ + public FlywayException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + /** + * Creates a new FlywayException with this message and this cause. + * + * @param message The exception message. + * @param cause The exception cause. + */ + public FlywayException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new FlywayException with this cause. For use in subclasses that override getMessage(). + * + * @param cause The exception cause. + */ + public FlywayException(Throwable cause) { + super(cause); + } + + /** + * Creates a new FlywayException with this message. + * + * @param message The exception message. + */ + public FlywayException(String message) { + super(message); + } + + /** + * Creates a new FlywayException. For use in subclasses that override getMessage(). + */ + public FlywayException() { + super(); + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/InfoOutputProvider.java b/flyway-core/src/main/java/org/flywaydb/core/api/InfoOutputProvider.java new file mode 100644 index 00000000..2b7016d0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/InfoOutputProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +import org.flywaydb.core.internal.output.InfoOutput; + +interface InfoOutputProvider { + InfoOutput getInfoOutput(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/Location.java b/flyway-core/src/main/java/org/flywaydb/core/api/Location.java new file mode 100644 index 00000000..d800c08b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/Location.java @@ -0,0 +1,318 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A location to load migrations from. + */ +public final class Location implements Comparable { + private static final Log LOG = LogFactory.getLog(Location.class); + + /** + * The prefix for classpath locations. + */ + private static final String CLASSPATH_PREFIX = "classpath:"; + + /** + * The prefix for filesystem locations. + */ + public static final String FILESYSTEM_PREFIX = "filesystem:"; + + /** + * The prefix part of the location. Can be either classpath: or filesystem:. + */ + private final String prefix; + + /** + * The path part of the location. + */ + private String rawPath; + + /** + * The first folder in the path. This will equal rawPath if the path does not contain any wildcards + */ + private String rootPath; + + private Pattern pathRegex = null; + + /** + * Creates a new location. + * + * @param descriptor The location descriptor. + */ + public Location(String descriptor) { + String normalizedDescriptor = descriptor.trim(); + + if (normalizedDescriptor.contains(":")) { + prefix = normalizedDescriptor.substring(0, normalizedDescriptor.indexOf(":") + 1); + rawPath = normalizedDescriptor.substring(normalizedDescriptor.indexOf(":") + 1); + } else { + prefix = CLASSPATH_PREFIX; + rawPath = normalizedDescriptor; + } + + if (isClassPath()) { + if (rawPath.contains(".")) { + LOG.warn("Use of dots (.) as path separators will be deprecated in Flyway 7. Path: " + rawPath); + } + rawPath = rawPath.replace(".", "/"); + if (rawPath.startsWith("/")) { + rawPath = rawPath.substring(1); + } + if (rawPath.endsWith("/")) { + rawPath = rawPath.substring(0, rawPath.length() - 1); + } + processRawPath(); + } else if (isFileSystem()) { + processRawPath(); + rootPath = new File(rootPath).getPath(); + + if (pathRegex == null) { + // if the original path contained no wildcards, also normalise it + rawPath = new File(rawPath).getPath(); + } + } else { + throw new FlywayException("Unknown prefix for location (should be either filesystem: or classpath:): " + + normalizedDescriptor); + } + + if (rawPath.endsWith(File.separator)) { + rawPath = rawPath.substring(0, rawPath.length() - 1); + } + } + + /** + * Process the rawPath into a rootPath and a regex. + * Supported wildcards: + * **: Match any 0 or more directories + * *: Match any sequence of non-seperator characters + * ?: Match any single character + */ + private void processRawPath() { + if (rawPath.contains("*") || rawPath.contains("?")) { + // we need to figure out the root, and create the regex + + String seperator = isFileSystem() ? File.separator : "/"; + String escapedSeperator = seperator.replace("\\", "\\\\").replace("/", "\\/"); + + // split on either of the path seperators + String[] pathSplit = rawPath.split("[\\\\/]"); + + StringBuilder rootPart = new StringBuilder(); + StringBuilder patternPart = new StringBuilder(); + + boolean endsInFile = false; + boolean skipSeperator = false; + boolean inPattern = false; + for (String pathPart : pathSplit) { + endsInFile = false; + + if (pathPart.contains("*") || pathPart.contains("?")) { + inPattern = true; + } + + if (inPattern) { + if (skipSeperator) { + skipSeperator = false; + } else { + patternPart.append("/"); + } + + String regex; + if ("**".equals(pathPart)) { + regex = "([^/]+/)*?"; + + // this pattern contains the ending seperator, so make sure we skip appending it after + skipSeperator = true; + } else { + endsInFile = pathPart.contains("."); + + regex = pathPart; + regex = regex.replace(".", "\\."); + regex = regex.replace("?", "[^/]"); + regex = regex.replace("*", "[^/]+?"); + } + + patternPart.append(regex); + } else { + rootPart.append(seperator).append(pathPart); + } + } + + // We always append a seperator before each part, so ensure we skip it when setting the final rootPath + rootPath = rootPart.length() > 0 ? rootPart.toString().substring(1) : ""; + + // Again, skip first seperator + String pattern = patternPart.toString().substring(1); + + // Replace the temporary / with the actual escaped seperator + pattern = pattern.replace("/", escapedSeperator); + + // Append the rootpath if it is non-empty + if (rootPart.length() > 0) { + pattern = rootPath.replace(seperator, escapedSeperator) + escapedSeperator + pattern; + } + + // if the path did not end in a file, then append the file match pattern + if (!endsInFile) { + pattern = pattern + escapedSeperator + "(?.*)"; + } + + pathRegex = Pattern.compile(pattern); + } else { + rootPath = rawPath; + } + } + + /** + * @return Whether the given path matches this locations regex. Will always return true when the location did not contain any wildcards. + */ + public boolean matchesPath(String path) { + if (pathRegex == null) { + return true; + } + + return pathRegex.matcher(path).matches(); + } + + /** + * Returns the path relative to this location. If the location path contains wildcards, the returned path will be relative + * to the last non-wildcard folder in the path. + * @return the path relative to this location + */ + public String getPathRelativeToThis(String path) { + if (pathRegex != null && pathRegex.pattern().contains("?")) { + Matcher matcher = pathRegex.matcher(path); + if (matcher.matches()) { + String relPath = matcher.group("relpath"); + if (relPath != null && relPath.length() > 0) { + return relPath; + } + } + } + + return rootPath.length() > 0 ? path.substring(rootPath.length() + 1) : path; + } + + /** + * Checks whether this denotes a location on the classpath. + * + * @return {@code true} if it does, {@code false} if it doesn't. + */ + public boolean isClassPath() { + return CLASSPATH_PREFIX.equals(prefix); + } + + /** + * Checks whether this denotes a location on the filesystem. + * + * @return {@code true} if it does, {@code false} if it doesn't. + */ + public boolean isFileSystem() { + return FILESYSTEM_PREFIX.equals(prefix); + } + + /** + * Checks whether this location is a parent of this other location. + * + * @param other The other location. + * @return {@code true} if it is, {@code false} if it isn't. + */ + @SuppressWarnings("SimplifiableIfStatement") + public boolean isParentOf(Location other) { + if (pathRegex != null || other.pathRegex != null) { + return false; + } + + if (isClassPath() && other.isClassPath()) { + return (other.getDescriptor() + "/").startsWith(getDescriptor() + "/"); + } + if (isFileSystem() && other.isFileSystem()) { + return (other.getDescriptor() + File.separator).startsWith(getDescriptor() + File.separator); + } + return false; + } + + /** + * @return The prefix part of the location. Can be either classpath: or filesystem:. + */ + public String getPrefix() { + return prefix; + } + + /** + * @return The root part of the path part of the location. + */ + public String getRootPath() { + return rootPath; + } + + /** + * @return The path part of the location. + */ + public String getPath() { + return rawPath; + } + + /** + * @return The the regex that matches in original path. Null if the original path did not contain any wildcards. + */ + public Pattern getPathRegex() { + return pathRegex; + } + + /** + * @return The complete location descriptor. + */ + public String getDescriptor() { + return prefix + rawPath; + } + + @SuppressWarnings("NullableProblems") + public int compareTo(Location o) { + return getDescriptor().compareTo(o.getDescriptor()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Location location = (Location) o; + + return getDescriptor().equals(location.getDescriptor()); + } + + @Override + public int hashCode() { + return getDescriptor().hashCode(); + } + + /** + * @return The complete location descriptor. + */ + @Override + public String toString() { + return getDescriptor(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfo.java b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfo.java new file mode 100644 index 00000000..a7895aeb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +import java.util.Date; + +/** + * Info about a migration. + */ +public interface MigrationInfo extends Comparable { + /** + * @return The type of migration (BASELINE, SQL, JDBC, ...) + */ + MigrationType getType(); + + /** + * @return The target version of this migration. + */ + Integer getChecksum(); + + /** + * @return The schema version after the migration is complete. + */ + MigrationVersion getVersion(); + + /** + * @return The description of the migration. + */ + String getDescription(); + + /** + * @return The name of the script to execute for this migration, relative to its classpath or filesystem location. + */ + String getScript(); + + /** + * @return The state of the migration (PENDING, SUCCESS, ...) + */ + MigrationState getState(); + + /** + * @return The timestamp when this migration was installed. (Only for applied migrations) + */ + Date getInstalledOn(); + + /** + * @return The user that installed this migration. (Only for applied migrations) + */ + String getInstalledBy(); + + /** + * @return The rank of this installed migration. This is the most precise way to sort applied migrations by installation order. + * Migrations that were applied later have a higher rank. (Only for applied migrations) + */ + Integer getInstalledRank(); + + /** + * @return The execution time (in millis) of this migration. (Only for applied migrations) + */ + Integer getExecutionTime(); + + /** + * @return The physical location of the migration on disk. + */ + String getPhysicalLocation(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfoService.java b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfoService.java new file mode 100644 index 00000000..78884618 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfoService.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +/** + * Info about all migrations, including applied, current and pending with details and status. + */ +public interface MigrationInfoService extends InfoOutputProvider { + /** + * Retrieves the full set of infos about applied, current and future migrations. + * + * @return The full set of infos. An empty array if none. + */ + MigrationInfo[] all(); + + /** + * Retrieves the information of the current applied migration, if any. + * + * @return The info. {@code null} if no migrations have been applied yet. + */ + MigrationInfo current(); + + /** + * Retrieves the full set of infos about pending migrations, available locally, but not yet applied to the DB. + * + * @return The pending migrations. An empty array if none. + */ + MigrationInfo[] pending(); + + /** + * Retrieves the full set of infos about the migrations applied to the DB. + * + * @return The applied migrations. An empty array if none. + */ + MigrationInfo[] applied(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationState.java b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationState.java new file mode 100644 index 00000000..1e279762 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationState.java @@ -0,0 +1,192 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +/** + * The state of a migration. + */ +public enum MigrationState { + /** + * This migration has not been applied yet. + */ + PENDING("Pending", true, false, false), + + /** + * This migration has not been applied yet, and won't be applied because target is set to a lower version. + */ + ABOVE_TARGET("Above Target", true, false, false), + + /** + * This migration was not applied against this DB, because the schema history table was baselined with a higher version. + */ + BELOW_BASELINE("Below Baseline", true, false, false), + + /** + * This migration has baselined this DB. + */ + BASELINE("Baseline", true, true, false), + + /** + *

This usually indicates a problem.

+ *

+ * This migration was not applied against this DB, because a migration with a higher version has already been + * applied. This probably means some checkins happened out of order. + *

+ *

Fix by increasing the version number, run clean and migrate again or rerun migration with outOfOrder enabled.

+ */ + IGNORED("Ignored", true, false, false), + + /** + *

This migration succeeded.

+ *

+ * This migration was applied against this DB, but it is not available locally. + * This usually results from multiple older migration files being consolidated into a single one. + *

+ */ + MISSING_SUCCESS("Missing", false, true, false), + + /** + *

This migration failed.

+ *

+ * This migration was applied against this DB, but it is not available locally. + * This usually results from multiple older migration files being consolidated into a single one. + *

+ *

This should rarely, if ever, occur in practice.

+ */ + MISSING_FAILED("Failed (Missing)", false, true, true), + + /** + * This migration succeeded. + */ + SUCCESS("Success", true, true, false), + + /** + * This versioned migration succeeded, but has since been undone. + */ + UNDONE("Undone", true, true, false), + + /** + * This undo migration is ready to be applied if desired. + */ + AVAILABLE("Available", true, false, false), + + /** + * This migration failed. + */ + FAILED("Failed", true, true, true), + + /** + *

This migration succeeded.

+ *

+ * This migration succeeded, but it was applied out of order. + * Rerunning the entire migration history might produce different results! + *

+ */ + OUT_OF_ORDER("Out of Order", true, true, false), + + /** + *

This migration succeeded.

+ *

+ * This migration has been applied against the DB, but it is not available locally. + * Its version is higher than the highest version available locally. + * It was most likely successfully installed by a future version of this deployable. + *

+ */ + FUTURE_SUCCESS("Future", false, true, false), + + /** + *

This migration failed.

+ *

+ * This migration has been applied against the DB, but it is not available locally. + * Its version is higher than the highest version available locally. + * It most likely failed during the installation of a future version of this deployable. + *

+ */ + FUTURE_FAILED("Failed (Future)", false, true, true), + + /** + * This is a repeatable migration that is outdated and should be re-applied. + */ + OUTDATED("Outdated", true, true, false), + + /** + * This is a repeatable migration that is outdated and has already been superseded by a newer run. + */ + SUPERSEDED("Superseded", true, true, false); + + /** + * The name suitable for display to the end-user. + */ + private final String displayName; + + /** + * Flag indicating if this migration is available on the classpath or not. + */ + private final boolean resolved; + + /** + * Flag indicating if this migration has been applied or not. + */ + private final boolean applied; + + /** + * Flag indicating if this migration has failed when it was applied or not. + */ + private final boolean failed; + + /** + * Creates a new MigrationState. + * + * @param displayName The name suitable for display to the end-user. + * @param resolved Flag indicating if this migration is available on the classpath or not. + * @param applied Flag indicating if this migration has been applied or not. + * @param failed Flag indicating if this migration has failed when it was applied or not. + */ + MigrationState(String displayName, boolean resolved, boolean applied, boolean failed) { + this.displayName = displayName; + this.resolved = resolved; + this.applied = applied; + this.failed = failed; + } + + /** + * @return The name suitable for display to the end-user. + */ + public String getDisplayName() { + return displayName; + } + + /** + * @return Flag indicating if this migration has been applied or not. + */ + public boolean isApplied() { + return applied; + } + + /** + * @return Flag indicating if this migration has been resolved or not. + */ + public boolean isResolved() { + return resolved; + } + + /** + * @return Flag indicating if this migration has failed or not. + */ + public boolean isFailed() { + return failed; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationType.java b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationType.java new file mode 100644 index 00000000..02e03fa7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationType.java @@ -0,0 +1,100 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +/** + * Type of migration. + */ +public enum MigrationType { + /** + * Schema creation migration. + */ + SCHEMA(true, false), + + /** + * Baseline migration. + */ + BASELINE(true, false), + + /** + * SQL migrations. + */ + SQL(false, false), + + /** + * Undo SQL migrations. + */ + UNDO_SQL(false, true), + + /** + * JDBC Java-based migrations. + */ + JDBC(false, false), + + /** + * Undo JDBC java-based migrations. + */ + UNDO_JDBC(false, true), + + /** + * Spring JDBC Java-based migrations. + * + * @deprecated Will be removed in Flyway 7.0. Use JDBC instead. + */ + @Deprecated + SPRING_JDBC(false, false), + + /** + * Undo Spring JDBC java-based migrations. + * + * @deprecated Will be removed in Flyway 7.0. Use UNDO_JDBC instead. + */ + @Deprecated + UNDO_SPRING_JDBC(false, true), + + /** + * Migrations using custom MigrationResolvers. + */ + CUSTOM(false, false), + + /** + * Undo migrations using custom MigrationResolvers. + */ + UNDO_CUSTOM(false, true); + + private final boolean synthetic; + private final boolean undo; + + MigrationType(boolean synthetic, boolean undo) { + this.synthetic = synthetic; + this.undo = undo; + } + + /** + * @return Whether this is a synthetic migration type, which is only ever present in the schema history table, + * but never discovered by migration resolvers. + */ + public boolean isSynthetic() { + return synthetic; + } + + /** + * @return Whether this is an undo migration, which has undone an earlier migration present in the schema history table. + */ + public boolean isUndo() { + return undo; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationVersion.java b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationVersion.java new file mode 100644 index 00000000..2f08f0ff --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationVersion.java @@ -0,0 +1,261 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * A version of a migration. + * + * @author Axel Fontaine + */ +public final class MigrationVersion implements Comparable { + /** + * Version for an empty schema. + */ + public static final MigrationVersion EMPTY = new MigrationVersion(null, "<< Empty Schema >>"); + + /** + * Latest version. + */ + public static final MigrationVersion LATEST = new MigrationVersion(BigInteger.valueOf(-1), "<< Latest Version >>"); + + /** + * Current version. Only a marker. For the real version use Flyway.info().current() instead. + */ + public static final MigrationVersion CURRENT = new MigrationVersion(BigInteger.valueOf(-2), "<< Current Version >>"); + + /** + * Regex for matching proper version format + */ + private static final Pattern SPLIT_REGEX = Pattern.compile("\\.(?=\\d)"); + + /** + * The individual parts this version string is composed of. Ex. 1.2.3.4.0 -> [1, 2, 3, 4, 0] + */ + private final List versionParts; + + /** + * The printable text to represent the version. + */ + private final String displayText; + + /** + * Create a MigrationVersion from a version String. + * + * @param version The version String. The value {@code current} will be interpreted as MigrationVersion.CURRENT, + * a marker for the latest version that has been applied to the database. + * @return The MigrationVersion + */ + @SuppressWarnings("ConstantConditions") + public static MigrationVersion fromVersion(String version) { + if ("current".equalsIgnoreCase(version)) return CURRENT; + if ("latest".equalsIgnoreCase(version) || LATEST.getVersion().equals(version)) return LATEST; + if (version == null) return EMPTY; + return new MigrationVersion(version); + } + + /** + * Creates a Version using this version string. + * + * @param version The version in one of the following formats: 6, 6.0, 005, 1.2.3.4, 201004200021.
{@code null} + * means that this version refers to an empty schema. + */ + private MigrationVersion(String version) { + String normalizedVersion = version.replace('_', '.'); + this.versionParts = tokenize(normalizedVersion); + this.displayText = normalizedVersion; + } + + /** + * Creates a Version using this version string. + * + * @param version The version in one of the following formats: 6, 6.0, 005, 1.2.3.4, 201004200021.
{@code null} + * means that this version refers to an empty schema. + * @param displayText The alternative text to display instead of the version number. + */ + private MigrationVersion(BigInteger version, String displayText) { + this.versionParts = new ArrayList<>(); + this.versionParts.add(version); + this.displayText = displayText; + } + + /** + * @return The textual representation of the version. + */ + @Override + public String toString() { + return displayText; + } + + /** + * @return Numeric version as String + */ + public String getVersion() { + if (this.equals(EMPTY)) return null; + if (this.equals(LATEST)) return Long.toString(Long.MAX_VALUE); + return displayText; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MigrationVersion version1 = (MigrationVersion) o; + + return compareTo(version1) == 0; + } + + @Override + public int hashCode() { + return versionParts == null ? 0 : versionParts.hashCode(); + } + + /** + * Convenience method for quickly checking whether this version is at least as new as this other version. + * + * @param otherVersion The other version. + * @return {@code true} if this version is equal or newer, {@code false} if it is older. + */ + public boolean isAtLeast(String otherVersion) { + return compareTo(MigrationVersion.fromVersion(otherVersion)) >= 0; + } + + /** + * Convenience method for quickly checking whether this version is newer than this other version. + * + * @param otherVersion The other version. + * @return {@code true} if this version is newer, {@code false} if it is not. + */ + public boolean isNewerThan(String otherVersion) { + return compareTo(MigrationVersion.fromVersion(otherVersion)) > 0; + } + + /** + * Convenience method for quickly checking whether this major version is newer than this other major version. + * + * @param otherVersion The other version. + * @return {@code true} if this major version is newer, {@code false} if it is not. + */ + public boolean isMajorNewerThan(String otherVersion) { + return getMajor().compareTo(MigrationVersion.fromVersion(otherVersion).getMajor()) > 0; + } + + /** + * @return The major version. + */ + public BigInteger getMajor() { + return versionParts.get(0); + } + + /** + * @return The major version as a string. + */ + public String getMajorAsString() { + return versionParts.get(0).toString(); + } + + /** + * @return The minor version as a string. + */ + public String getMinorAsString() { + if (versionParts.size() == 1) { + return "0"; + } + return versionParts.get(1).toString(); + } + + @Override + public int compareTo(MigrationVersion o) { + if (o == null) { + return 1; + } + + if (this == EMPTY) { + if (o == EMPTY) return 0; + else return -1; + } + + if (this == CURRENT) { + return o == CURRENT ? 0 : -1; + } + + if (this == LATEST) { + if (o == LATEST) return 0; + else return 1; + } + + if (o == EMPTY) { + return 1; + } + + if (o == CURRENT) { + return 1; + } + + if (o == LATEST) { + return -1; + } + final List parts1 = versionParts; + final List parts2 = o.versionParts; + int largestNumberOfParts = Math.max(parts1.size(), parts2.size()); + for (int i = 0; i < largestNumberOfParts; i++) { + final int compared = getOrZero(parts1, i).compareTo(getOrZero(parts2, i)); + if (compared != 0) { + return compared; + } + } + return 0; + } + + private BigInteger getOrZero(List elements, int i) { + return i < elements.size() ? elements.get(i) : BigInteger.ZERO; + } + + /** + * Splits this string into list of Long + * + * @param versionStr The string to split. + * @return The resulting array. + */ + private List tokenize(String versionStr) { + List parts = new ArrayList<>(); + for (String part : SPLIT_REGEX.split(versionStr)) { + parts.add(toBigInteger(versionStr, part)); + } + + for (int i = parts.size() - 1; i > 0; i--) { + if (!parts.get(i).equals(BigInteger.ZERO)) { + break; + } + parts.remove(i); + } + + return parts; + } + + private BigInteger toBigInteger(String versionStr, String part) { + try { + return new BigInteger(part); + } catch (NumberFormatException e) { + throw new FlywayException("Version may only contain 0..9 and . (dot). Invalid version: " + versionStr); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/android/ContextHolder.java b/flyway-core/src/main/java/org/flywaydb/core/api/android/ContextHolder.java new file mode 100644 index 00000000..b10abcdd --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/android/ContextHolder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.android; + +import android.content.Context; + +/** + * Holds an Android context. The context must be set for Flyway to be able to scan assets and classes for migrations. + * + *

+ * You can set this within an activity using ContextHolder.setContext(this); + *

+ */ +public class ContextHolder { + private ContextHolder() {} + + /** + * The Android context to use. + */ + private static Context context; + + /** + * @return The Android context to use to be able to scan assets and classes for migrations. + */ + public static Context getContext() { + return context; + } + + /** + * @param context The Android context to use to be able to scan assets and classes for migrations. + */ + public static void setContext(Context context) { + ContextHolder.context = context; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/android/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/android/package-info.java new file mode 100644 index 00000000..694c4076 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/android/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Android-specific helper classes. + */ +package org.flywaydb.core.api.android; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/BaseCallback.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/BaseCallback.java new file mode 100644 index 00000000..07fdf009 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/BaseCallback.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +/** + * Base implementation of Callback from which one can inherit. This is a convenience class that assumes by default that + * all events are handled and all handlers can run within a transaction. + */ +public abstract class BaseCallback implements Callback { + @Override + public boolean supports(Event event, Context context) { + return true; + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Callback.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Callback.java new file mode 100644 index 00000000..eae83cf7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Callback.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +/** + * This is the main callback interface that should be implemented to handle Flyway lifecycle events. + */ +public interface Callback { + /** + * Whether this callback supports this event or not. This is primarily meant as a way to optimize event handling + * by avoiding unnecessary connection state setups for events that will not be handled anyway. + * + * @param event The event to check. + * @param context The context for this event. + * @return {@code true} if it can be handled, {@code false} if not. + */ + boolean supports(Event event, Context context); + + /** + * Whether this event can be handled in a transaction or whether it must be handled outside a transaction instead. + * In the vast majority of the cases the answer will be + * {@code true}. Only in the rare cases where non-transactional statements are executed should this return {@code false}. + * This method is called before {@link #handle(Event, Context)} in order to determine in advance whether a transaction + * can be used or not. + * + * @param event The event to check. + * @param context The context for this event. + * @return {@code true} if it can be handled within a transaction (almost all cases). {@code false} if it must be + * handled outside a transaction instead (very rare). + */ + boolean canHandleInTransaction(Event event, Context context); + + /** + * Handles this Flyway lifecycle event. + * + * @param event The event to handle. + * @param context The context for this event. + */ + void handle(Event event, Context context); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Context.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Context.java new file mode 100644 index 00000000..0c9acb96 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Context.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.configuration.Configuration; + +import java.sql.Connection; + +/** + * The context relevant to an event. + */ +public interface Context { + /** + * @return The configuration currently in use. + */ + Configuration getConfiguration(); + + /** + * @return The JDBC connection being used. Transaction are managed by Flyway. + * When the context is passed to the {@link Callback#handle(Event, Context)} method, a transaction will already have + * been started if required and will be automatically committed or rolled back afterwards. + */ + Connection getConnection(); + + /** + * @return The info about the migration being handled. Only relevant for the BEFORE_EACH_* and AFTER_EACH_* events. + * {@code null} in all other cases. + */ + MigrationInfo getMigrationInfo(); + + /** + * @return The info about the statement being handled. Only relevant for the statement-level events. + * {@code null} in all other cases. + *

Flyway Pro and Flyway Enterprise only

+ */ + Statement getStatement(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Error.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Error.java new file mode 100644 index 00000000..14d96c55 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Error.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +/** + * An error that occurred while executing a statement. + *

Flyway Pro and Flyway Enterprise only

+ */ +public interface Error { + /** + * @return The error code. + */ + int getCode(); + + /** + * @return The error state. + */ + String getState(); + + /** + * @return The error message. + */ + String getMessage(); + + /** + * Checks whether this error has already been handled. + * + * @return {@code true} {@code true} if this error has already be handled or {@code false} if it should flow + * via the default error handler. + */ + boolean isHandled(); + + /** + * Sets whether this error has already been handled. + * + * @param handled {@code true} if this error has already be handled or {@code false} if it should flow via the + * default error handler. + */ + void setHandled(boolean handled); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java new file mode 100644 index 00000000..7cc501d7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java @@ -0,0 +1,216 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +/** + * The Flyway lifecycle events that can be handled in callbacks. + */ +public enum Event { + /** + * Fired before clean is executed. This event will be fired in a separate transaction from the actual clean operation. + */ + BEFORE_CLEAN("beforeClean"), + /** + * Fired after clean has succeeded. This event will be fired in a separate transaction from the actual clean operation. + */ + AFTER_CLEAN("afterClean"), + /** + * Fired after clean has failed. This event will be fired in a separate transaction from the actual clean operation. + */ + AFTER_CLEAN_ERROR("afterCleanError"), + + /** + * Fired before migrate is executed. This event will be fired in a separate transaction from the actual migrate operation. + */ + BEFORE_MIGRATE("beforeMigrate"), + /** + * Fired before each individual migration is executed. This event will be fired within the same transaction (if any) + * as the migration and can be used for things like setting up connection parameters that are required by migrations. + */ + BEFORE_EACH_MIGRATE("beforeEachMigrate"), + /** + * Fired before each individual statement in a migration is executed. This event will be fired within the same transaction (if any) + * as the migration and can be used for things like asserting a statement complies with policy (for example: no grant statements allowed). + *

Flyway Pro and Enterprise Edition only

+ */ + BEFORE_EACH_MIGRATE_STATEMENT("beforeEachMigrateStatement"), + /** + * Fired after each individual statement in a migration that succeeded. This event will be fired within the same transaction (if any) + * as the migration. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_MIGRATE_STATEMENT("afterEachMigrateStatement"), + /** + * Fired after each individual statement in a migration that failed. This event will be fired within the same transaction (if any) + * as the migration. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_MIGRATE_STATEMENT_ERROR("afterEachMigrateStatementError"), + /** + * Fired after each individual migration that succeeded. This event will be fired within the same transaction (if any) + * as the migration. + */ + AFTER_EACH_MIGRATE("afterEachMigrate"), + /** + * Fired after each individual migration that failed. This event will be fired within the same transaction (if any) + * as the migration. + */ + AFTER_EACH_MIGRATE_ERROR("afterEachMigrateError"), + /** + * Fired after migrate has succeeded. This event will be fired in a separate transaction from the actual migrate operation. + */ + AFTER_MIGRATE("afterMigrate"), + /** + * Fired after migrate has failed. This event will be fired in a separate transaction from the actual migrate operation. + */ + AFTER_MIGRATE_ERROR("afterMigrateError"), + + /** + * Fired before undo is executed. This event will be fired in a separate transaction from the actual undo operation. + *

Flyway Pro and Enterprise Edition only

+ */ + BEFORE_UNDO("beforeUndo"), + /** + * Fired before each individual undo is executed. This event will be fired within the same transaction (if any) + * as the undo and can be used for things like setting up connection parameters that are required by undo. + *

Flyway Pro and Enterprise Edition only

+ */ + BEFORE_EACH_UNDO("beforeEachUndo"), + /** + * Fired before each individual statement in an undo migration is executed. This event will be fired within the same transaction (if any) + * as the migration and can be used for things like asserting a statement complies with policy (for example: no grant statements allowed). + *

Flyway Pro and Enterprise Edition only

+ */ + BEFORE_EACH_UNDO_STATEMENT("beforeEachUndoStatement"), + /** + * Fired after each individual statement in an undo migration that succeeded. This event will be fired within the same transaction (if any) + * as the migration. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_UNDO_STATEMENT("afterEachUndoStatement"), + /** + * Fired after each individual statement in an undo migration that failed. This event will be fired within the same transaction (if any) + * as the migration. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_UNDO_STATEMENT_ERROR("afterEachUndoStatementError"), + /** + * Fired after each individual undo that succeeded. This event will be fired within the same transaction (if any) + * as the undo. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_UNDO("afterEachUndo"), + /** + * Fired after each individual undo that failed. This event will be fired within the same transaction (if any) + * as the undo. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_EACH_UNDO_ERROR("afterEachUndoError"), + /** + * Fired after undo has succeeded. This event will be fired in a separate transaction from the actual undo operation. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_UNDO("afterUndo"), + /** + * Fired after undo has failed. This event will be fired in a separate transaction from the actual undo operation. + *

Flyway Pro and Enterprise Edition only

+ */ + AFTER_UNDO_ERROR("afterUndoError"), + + /** + * Fired before validate is executed. This event will be fired in a separate transaction from the actual validate operation. + */ + BEFORE_VALIDATE("beforeValidate"), + /** + * Fired after validate has succeeded. This event will be fired in a separate transaction from the actual validate operation. + */ + AFTER_VALIDATE("afterValidate"), + /** + * Fired after validate has failed. This event will be fired in a separate transaction from the actual validate operation. + */ + AFTER_VALIDATE_ERROR("afterValidateError"), + + /** + * Fired before baseline is executed. This event will be fired in a separate transaction from the actual baseline operation. + */ + BEFORE_BASELINE("beforeBaseline"), + /** + * Fired after baseline has succeeded. This event will be fired in a separate transaction from the actual baseline operation. + */ + AFTER_BASELINE("afterBaseline"), + /** + * Fired after baseline has failed. This event will be fired in a separate transaction from the actual baseline operation. + */ + AFTER_BASELINE_ERROR("afterBaselineError"), + + /** + * Fired before repair is executed. This event will be fired in a separate transaction from the actual repair operation. + */ + BEFORE_REPAIR("beforeRepair"), + /** + * Fired after repair has succeeded. This event will be fired in a separate transaction from the actual repair operation. + */ + AFTER_REPAIR("afterRepair"), + /** + * Fired after repair has failed. This event will be fired in a separate transaction from the actual repair operation. + */ + AFTER_REPAIR_ERROR("afterRepairError"), + + /** + * Fired before info is executed. This event will be fired in a separate transaction from the actual info operation. + */ + BEFORE_INFO("beforeInfo"), + /** + * Fired after info has succeeded. This event will be fired in a separate transaction from the actual info operation. + */ + AFTER_INFO("afterInfo"), + /** + * Fired after info has failed. This event will be fired in a separate transaction from the actual info operation. + */ + AFTER_INFO_ERROR("afterInfoError"); + + private final String id; + + Event(String id) { + this.id = id; + } + + /** + * @return The id of an event. Examples: {@code beforeClean}, {@code afterEachMigrate}, ... + */ + public String getId() { + return id; + } + + /** + * Retrieves the event with this id. + * @param id The id. + * @return The event. {@code null} if not found. + */ + public static Event fromId(String id) { + for (Event event : values()) { + if (event.id.equals(id)) { + return event; + } + } + return null; + } + + @Override + public String toString() { + return id; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Statement.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Statement.java new file mode 100644 index 00000000..891b02b9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Statement.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +import java.util.List; + +/** + * The statement relevant to an event. + *

Flyway Pro and Flyway Enterprise only

+ */ +public interface Statement { + /** + * @return The SQL statement. + */ + String getSql(); + + /** + * @return The warnings that were raised during the execution of the statement. + * {@code null} if the statement hasn't been executed yet. + */ + List getWarnings(); + + /** + * @return The errors that were thrown during the execution of the statement. + * {@code null} if the statement hasn't been executed yet. + */ + List getErrors(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Warning.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Warning.java new file mode 100644 index 00000000..eaec7071 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Warning.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.callback; + +/** + * A warning that occurred while executing a statement. + *

Flyway Pro and Flyway Enterprise only

+ */ +public interface Warning { + /** + * @return The warning code. + */ + int getCode(); + + /** + * @return The warning state. + */ + String getState(); + + /** + * @return The warning message. + */ + String getMessage(); + + /** + * Checks whether this warning has already been handled. + * + * @return {@code true} {@code true} if this warning has already be handled or {@code false} if it should flow + * via the default warning handler. + */ + boolean isHandled(); + + /** + * Sets whether this warning has already been handled. + * + * @param handled {@code true} if this warning has already be handled or {@code false} if it should flow via the + * default warning handler. + */ + void setHandled(boolean handled); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/callback/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/callback/package-info.java new file mode 100644 index 00000000..b92cd6f5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/callback/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Flyway lifecycle callbacks. + */ +package org.flywaydb.core.api.callback; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java new file mode 100644 index 00000000..a5765c3e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java @@ -0,0 +1,1924 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.configuration; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.internal.jdbc.DriverDataSource; +import org.flywaydb.core.internal.license.Edition; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.Locations; +import org.flywaydb.core.internal.util.StringUtils; + +import javax.sql.DataSource; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.flywaydb.core.internal.configuration.ConfigUtils.removeBoolean; +import static org.flywaydb.core.internal.configuration.ConfigUtils.removeInteger; + +/** + * JavaBean-style configuration for Flyway. This is primarily meant for compatibility with scenarios where the + * new FluentConfiguration isn't an easy fit, such as Spring XML bean configuration. + *

+ * This configuration can then be passed to Flyway using the new Flyway(Configuration) constructor. + *

+ */ +public class ClassicConfiguration implements Configuration { + private static final Log LOG = LogFactory.getLog(ClassicConfiguration.class); + + private String driver; + private String url; + private String user; + private String password; + + /** + * The dataSource to use to access the database. Must have the necessary privileges to execute ddl. + */ + private DataSource dataSource; + + /** + * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will + * wait 1 second before attempting to connect again, up to the maximum number of times specified by connectRetries. + * (default: 0) + */ + private int connectRetries; + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * (default: {@code null}) + */ + private String initSql; + + /** + * The ClassLoader to use for resolving migrations on the classpath. (default: Thread.currentThread().getContextClassLoader() ) + */ + private ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + /** + * The locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both sql and java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem and may only contain sql + * migrations.

+ *

+ * (default: db/migration) + */ + private Locations locations = new Locations("db/migration"); + + /** + * The encoding of Sql migrations. (default: UTF-8) + */ + private Charset encoding = StandardCharsets.UTF_8; + + /** + * The default schema managed by Flyway. This schema name is case-sensitive. If not specified, but + * schemaNames is, Flyway uses the first schema in that list. If that is also not specified, Flyway uses + * the default schema for the database connection. + *

Consequences:

+ *
    + *
  • This schema will be the one containing the schema history table.
  • + *
  • This schema will be the default for the database connection (provided the database supports this concept).
  • + *
+ */ + private String defaultSchemaName = null; + + /** + * The schemas managed by Flyway. These schema names are case-sensitive. If not specified, Flyway uses + * the default schema for the database connection. If defaultSchemaName is not specified, then the first of + * this list also acts as default schema. + *

Consequences:

+ *
    + *
  • Flyway will automatically attempt to create all these schemas, unless they already exist.
  • + *
  • The schemas will be cleaned in the order of this list.
  • + *
  • If Flyway created them, the schemas themselves will be dropped when cleaning.
  • + *
+ */ + private String[] schemaNames = {}; + + /** + *

The name of the schema history table that will be used by Flyway. (default: flyway_schema_history)

By default + * (single-schema mode) the schema history table is placed in the default schema for the connection provided by the + * datasource.

When the flyway.schemas property is set (multi-schema mode), the schema history table is + * placed in the first schema of the list.

+ */ + private String table = "flyway_schema_history"; + + /** + *

The tablespace where to create the schema history table that will be used by Flyway.

+ *

If not specified, Flyway uses the default tablespace for the database connection. + * This setting is only relevant for databases that do support the notion of tablespaces. Its value is simply + * ignored for all others.

+ */ + private String tablespace; + + /** + * The target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + */ + private MigrationVersion target; + + /** + * Whether placeholders should be replaced. (default: true) + */ + private boolean placeholderReplacement = true; + + /** + * The map of <placeholder, replacementValue> to apply to sql migration scripts. + */ + private Map placeholders = new HashMap<>(); + + /** + * The prefix of every placeholder. (default: ${ ) + */ + private String placeholderPrefix = "${"; + + /** + * The suffix of every placeholder. (default: } ) + */ + private String placeholderSuffix = "}"; + + /** + * The file name prefix for versioned SQL migrations. (default: V) + *

+ *

Versioned SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ */ + private String sqlMigrationPrefix = "V"; + + + + + + + + + + + + + /** + * The file name prefix for repeatable SQL migrations. (default: R) + *

+ *

Repeatable sql migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , + * which using the defaults translates to R__My_description.sql

+ */ + private String repeatableSqlMigrationPrefix = "R"; + + /** + * The file name separator for sql migrations. (default: __) + *

+ *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ */ + private String sqlMigrationSeparator = "__"; + + /** + * The file name suffixes for SQL migrations. (default: .sql) + *

SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ *

Multiple suffixes (like .sql,.pkg,.pkb) can be specified for easier compatibility with other tools such as + * editors with specific file associations.

+ */ + private String[] sqlMigrationSuffixes = {".sql"}; + + /** + * The manually added Java-based migrations. These are not Java-based migrations discovered through classpath + * scanning and instantiated by Flyway. Instead these are manually added instances of JavaMigration. + * This is particularly useful when working with a dependency injection container, where you may want the DI + * container to instantiate the class and wire up its dependencies for you. (default: none) + */ + private JavaMigration[] javaMigrations = {}; + + /** + * Ignore missing migrations when reading the schema history table. These are migrations that were performed by an + * older deployment of the application that are no longer available in this version. For example: we have migrations + * available on the classpath with versions 1.0 and 3.0. The schema history table indicates that a migration with version 2.0 + * (unknown to us) has also been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to deploy + * a newer version of the application even though it doesn't contain migrations included with an older one anymore. + * Note that if the most recently applied migration is removed, Flyway has no way to know it is missing and will + * mark it as future instead. + *

+ * {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + private boolean ignoreMissingMigrations; + + /** + * Ignore ignored migrations when reading the schema history table. These are migrations that were added in between + * already migrated migrations in this version. For example: we have migrations available on the classpath with + * versions from 1.0 to 3.0. The schema history table indicates that version 1 was finished on 1.0.15, and the next + * one was 2.0.0. But with the next release a new migration was added to version 1: 1.0.16. Such scenario is ignored + * by migrate command, but by default is rejected by validate. When ignoreIgnoredMigrations is enabled, such case + * will not be reported by validate command. This is useful for situations where one must be able to deliver + * complete set of migrations in a delivery package for multiple versions of the product, and allows for further + * development of older versions. + *

+ * {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + private boolean ignoreIgnoredMigrations; + + /** + * Ignore pending migrations when reading the schema history table. These are migrations that are available on the + * classpath but have not yet been performed by an application deployment. + * This can be useful for verifying that in-development migration changes don't contain any validation-breaking changes + * of migrations that have already been applied to a production environment, e.g. as part of a CI/CD process, without + * failing because of the existence of new migration versions. + *

+ * {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + private boolean ignorePendingMigrations; + + /** + * Ignore future migrations when reading the schema history table. These are migrations that were performed by a + * newer deployment of the application that are not yet available in this version. For example: we have migrations + * available on the classpath up to version 3.0. The schema history table indicates that a migration to version 4.0 + * (unknown to us) has already been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to redeploy + * an older version of the application after the database has been migrated by a newer one. (default: {@code true}) + */ + private boolean ignoreFutureMigrations = true; + + /** + * Whether to validate migrations and callbacks whose scripts do not obey the correct naming convention. A failure can be + * useful to check that errors such as case sensitivity in migration prefixes have been corrected. + * {@code false} to continue normally, {@code true} to fail fast with an exception. (default: {@code false}) + */ + private boolean validateMigrationNaming = false; + + /** + * Whether to automatically call validate or not when running migrate. (default: {@code true}) + */ + private boolean validateOnMigrate = true; + + /** + * Whether to automatically call clean or not when a validation error occurs. (default: {@code false}) + *

This is exclusively intended as a convenience for development. even though we + * strongly recommend not to change migration scripts once they have been checked into SCM and run, this provides a + * way of dealing with this case in a smooth manner. The database will be wiped clean automatically, ensuring that + * the next migration will bring you back to the state checked into SCM.

+ *

Warning ! Do not enable in production !

+ */ + private boolean cleanOnValidationError; + + /** + * Whether to disable clean. (default: {@code false}) + *

This is especially useful for production environments where running clean can be quite a career limiting move.

+ */ + private boolean cleanDisabled; + + /** + * The version to tag an existing schema with when executing baseline. (default: 1) + */ + private MigrationVersion baselineVersion = MigrationVersion.fromVersion("1"); + + /** + * The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + private String baselineDescription = "<< Flyway Baseline >>"; + + /** + *

+ * Whether to automatically call baseline when migrate is executed against a non-empty schema with no schema history table. + * This schema will then be initialized with the {@code baselineVersion} before executing the migrations. + * Only migrations above {@code baselineVersion} will then be applied. + *

+ *

+ * This is useful for initial Flyway production deployments on projects with an existing DB. + *

+ *

+ * Be careful when enabling this as it removes the safety net that ensures + * Flyway does not migrate the wrong database in case of a configuration mistake! (default: {@code false}) + *

+ */ + private boolean baselineOnMigrate; + + /** + * Allows migrations to be run "out of order". + *

If you already have versions 1 and 3 applied, and now a version 2 is found, + * it will be applied too instead of being ignored.

+ *

(default: {@code false})

+ */ + private boolean outOfOrder; + + /** + * This is a list of custom callbacks that fire before and after tasks are executed. You can + * add as many custom callbacks as you want. (default: none) + */ + private final List callbacks = new ArrayList<>(); + + /** + * Whether Flyway should skip the default callbacks. If true, only custom callbacks are used. + *

(default: false)

+ */ + private boolean skipDefaultCallbacks; + + /** + * The custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + *

(default: none)

+ */ + private MigrationResolver[] resolvers = new MigrationResolver[0]; + + /** + * Whether Flyway should skip the default resolvers. If true, only custom resolvers are used. + *

(default: false)

+ */ + private boolean skipDefaultResolvers; + + /** + * Whether to allow mixing transactional and non-transactional statements within the same migration. + *

+ * {@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false}) + */ + private boolean mixed; + + /** + * Whether to group all pending migrations together in the same transaction when applying them (only recommended for databases with support for DDL transactions). + *

+ * {@code true} if migrations should be grouped. {@code false} if they should be applied individually instead. (default: {@code false}) + */ + private boolean group; + + /** + * The username that will be recorded in the schema history table as having applied the migration. + *

+ * {@code null} for the current database user of the connection. (default: {@code null}). + */ + private String installedBy; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /** + * Creates a new default configuration. + */ + public ClassicConfiguration() { + // Nothing to do. + } + + /** + * Creates a new default configuration with this classloader. + * + * @param classLoader The ClassLoader to use for loading migrations, resolvers, etc from the classpath. (default: Thread.currentThread().getContextClassLoader() ) + */ + public ClassicConfiguration(ClassLoader classLoader) { + if (classLoader != null) { + this.classLoader = classLoader; + } + } + + /** + * Creates a new configuration with the same values as this existing one. + * + * @param configuration The configuration to use. + */ + public ClassicConfiguration(Configuration configuration) { + this(configuration.getClassLoader()); + configure(configuration); + } + + @Override + public Location[] getLocations() { + return locations.getLocations().toArray(new Location[0]); + } + + @Override + public Charset getEncoding() { + return encoding; + } + + @Override + public String getDefaultSchema() { return defaultSchemaName; } + + @Override + public String[] getSchemas() { return schemaNames; } + + @Override + public String getTable() { + return table; + } + + @Override + public String getTablespace() { + return tablespace; + } + + @Override + public MigrationVersion getTarget() { + return target; + } + + @Override + public boolean isPlaceholderReplacement() { + return placeholderReplacement; + } + + @Override + public Map getPlaceholders() { + return placeholders; + } + + @Override + public String getPlaceholderPrefix() { + return placeholderPrefix; + } + + @Override + public String getPlaceholderSuffix() { + return placeholderSuffix; + } + + @Override + public String getSqlMigrationPrefix() { + return sqlMigrationPrefix; + } + + @Override + public String getRepeatableSqlMigrationPrefix() { + return repeatableSqlMigrationPrefix; + } + + @Override + public String getSqlMigrationSeparator() { + return sqlMigrationSeparator; + } + + @Override + public String[] getSqlMigrationSuffixes() { + return sqlMigrationSuffixes; + } + + @Override + public JavaMigration[] getJavaMigrations() { + return javaMigrations; + } + + @Override + public boolean isIgnoreMissingMigrations() { + return ignoreMissingMigrations; + } + + @Override + public boolean isIgnoreIgnoredMigrations() { + return ignoreIgnoredMigrations; + } + + @Override + public boolean isIgnorePendingMigrations() { + return ignorePendingMigrations; + } + + @Override + public boolean isIgnoreFutureMigrations() { + return ignoreFutureMigrations; + } + + @Override + public boolean isValidateMigrationNaming() { + return validateMigrationNaming; + } + + @Override + public boolean isValidateOnMigrate() { + return validateOnMigrate; + } + + @Override + public boolean isCleanOnValidationError() { + return cleanOnValidationError; + } + + @Override + public boolean isCleanDisabled() { + return cleanDisabled; + } + + @Override + public MigrationVersion getBaselineVersion() { + return baselineVersion; + } + + @Override + public String getBaselineDescription() { + return baselineDescription; + } + + @Override + public boolean isBaselineOnMigrate() { + return baselineOnMigrate; + } + + @Override + public boolean isOutOfOrder() { + return outOfOrder; + } + + @Override + public MigrationResolver[] getResolvers() { + return resolvers; + } + + @Override + public boolean isSkipDefaultResolvers() { + return skipDefaultResolvers; + } + + @Override + public DataSource getDataSource() { + if (dataSource == null && + (StringUtils.hasLength(driver) || StringUtils.hasLength(user) || StringUtils.hasLength(password))) { + LOG.warn("Discarding INCOMPLETE dataSource configuration! " + ConfigUtils.URL + " must be set."); + } + return dataSource; + } + + @Override + public int getConnectRetries() { + return connectRetries; + } + + @Override + public String getInitSql() { + return initSql; + } + + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + @Override + public boolean isMixed() { + return mixed; + } + + @Override + public String getInstalledBy() { + return installedBy; + } + + @Override + public boolean isGroup() { + return group; + } + + @Override + public String[] getErrorOverrides() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("errorOverrides"); + + + + + } + + @Override + public OutputStream getDryRunOutput() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("dryRunOutput"); + + + + + } + + @Override + public String getLicenseKey() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("licenseKey"); + + + + + } + + /** + * Whether Flyway should output a table with the results of queries when executing migrations. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to output the results table (default: {@code true}) + */ + @Override + public boolean outputQueryResults() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("outputQueryResults"); + + + + + } + + /** + * Sets the stream where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. The stream when be closing when Flyway finishes writing the output. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutput The output file or {@code null} to execute the SQL statements directly against the database. + */ + public void setDryRunOutput(OutputStream dryRunOutput) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("dryRunOutput"); + + + + + } + + /** + * Sets the file where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. If the file specified is in a non-existent directory, Flyway will create all + * directories and parent directories as needed. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutput The output file or {@code null} to execute the SQL statements directly against the database. + */ + public void setDryRunOutputAsFile(File dryRunOutput) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("dryRunOutput"); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + + /** + * Sets the file where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. If the file specified is in a non-existent directory, Flyway will create all + * directories and parent directories as needed. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutputFileName The name of the output file or {@code null} to execute the SQL statements directly + * against the database. + */ + public void setDryRunOutputAsFileName(String dryRunOutputFileName) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("dryRunOutput"); + + + + + } + + /** + * Rules for the built-in error handler that let you override specific SQL states and errors codes in order to force + * specific errors or warnings to be treated as debug messages, info messages, warnings or errors. + *

Each error override has the following format: {@code STATE:12345:W}. + * It is a 5 character SQL state (or * to match all SQL states), a colon, + * the SQL error code (or * to match all SQL error codes), a colon and finally + * the desired behavior that should override the initial one.

+ *

The following behaviors are accepted:

+ *
    + *
  • {@code D} to force a debug message
  • + *
  • {@code D-} to force a debug message, but do not show the original sql state and error code
  • + *
  • {@code I} to force an info message
  • + *
  • {@code I-} to force an info message, but do not show the original sql state and error code
  • + *
  • {@code W} to force a warning
  • + *
  • {@code W-} to force a warning, but do not show the original sql state and error code
  • + *
  • {@code E} to force an error
  • + *
  • {@code E-} to force an error, but do not show the original sql state and error code
  • + *
+ *

Example 1: to force Oracle stored procedure compilation issues to produce + * errors instead of warnings, the following errorOverride can be used: {@code 99999:17110:E}

+ *

Example 2: to force SQL Server PRINT messages to be displayed as info messages (without SQL state and error + * code details) instead of warnings, the following errorOverride can be used: {@code S0001:0:I-}

+ *

Example 3: to force all errors with SQL error code 123 to be treated as warnings instead, + * the following errorOverride can be used: {@code *:123:W}

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @param errorOverrides The ErrorOverrides or an empty array if none are defined. (default: none) + */ + public void setErrorOverrides(String... errorOverrides) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("errorOverrides"); + + + + + } + + /** + * Whether to group all pending migrations together in the same transaction when applying them (only recommended for databases with support for DDL transactions). + * + * @param group {@code true} if migrations should be grouped. {@code false} if they should be applied individually instead. (default: {@code false}) + */ + public void setGroup(boolean group) { + this.group = group; + } + + /** + * The username that will be recorded in the schema history table as having applied the migration. + * + * @param installedBy The username or {@code null} for the current database user of the connection. (default: {@code null}). + */ + public void setInstalledBy(String installedBy) { + if ("".equals(installedBy)) { + installedBy = null; + } + this.installedBy = installedBy; + } + + /** + * Whether to allow mixing transactional and non-transactional statements within the same migration. Enabling this + * automatically causes the entire affected migration to be run without a transaction. + * + *

Note that this is only applicable for PostgreSQL, Aurora PostgreSQL, SQL Server and SQLite which all have + * statements that do not run at all within a transaction.

+ *

This is not to be confused with implicit transaction, as they occur in MySQL or Oracle, where even though a + * DDL statement was run within a transaction, the database will issue an implicit commit before and after + * its execution.

+ * + * @param mixed {@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false}) + */ + public void setMixed(boolean mixed) { + this.mixed = mixed; + } + + /** + * Ignore missing migrations when reading the schema history table. These are migrations that were performed by an + * older deployment of the application that are no longer available in this version. For example: we have migrations + * available on the classpath with versions 1.0 and 3.0. The schema history table indicates that a migration with version 2.0 + * (unknown to us) has also been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to deploy + * a newer version of the application even though it doesn't contain migrations included with an older one anymore. + * Note that if the most recently applied migration is removed, Flyway has no way to know it is missing and will + * mark it as future instead. + * + * @param ignoreMissingMigrations {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public void setIgnoreMissingMigrations(boolean ignoreMissingMigrations) { + this.ignoreMissingMigrations = ignoreMissingMigrations; + } + + /** + * Ignore ignored migrations when reading the schema history table. These are migrations that were added in between + * already migrated migrations in this version. For example: we have migrations available on the classpath with + * versions from 1.0 to 3.0. The schema history table indicates that version 1 was finished on 1.0.15, and the next + * one was 2.0.0. But with the next release a new migration was added to version 1: 1.0.16. Such scenario is ignored + * by migrate command, but by default is rejected by validate. When ignoreIgnoredMigrations is enabled, such case + * will not be reported by validate command. This is useful for situations where one must be able to deliver + * complete set of migrations in a delivery package for multiple versions of the product, and allows for further + * development of older versions. + * + * @param ignoreIgnoredMigrations {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public void setIgnoreIgnoredMigrations(boolean ignoreIgnoredMigrations) { + this.ignoreIgnoredMigrations = ignoreIgnoredMigrations; + } + + /** + * Ignore pending migrations when reading the schema history table. These are migrations that are available + * but have not yet been applied. This can be useful for verifying that in-development migration changes + * don't contain any validation-breaking changes of migrations that have already been applied to a production + * environment, e.g. as part of a CI/CD process, without failing because of the existence of new migration versions. + * + * @param ignorePendingMigrations {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public void setIgnorePendingMigrations(boolean ignorePendingMigrations) { + this.ignorePendingMigrations = ignorePendingMigrations; + } + + /** + * Whether to ignore future migrations when reading the schema history table. These are migrations that were performed by a + * newer deployment of the application that are not yet available in this version. For example: we have migrations + * available on the classpath up to version 3.0. The schema history table indicates that a migration to version 4.0 + * (unknown to us) has already been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to redeploy + * an older version of the application after the database has been migrated by a newer one. + * + * @param ignoreFutureMigrations {@code true} to continue normally and log a warning, {@code false} to fail + * fast with an exception. (default: {@code true}) + */ + public void setIgnoreFutureMigrations(boolean ignoreFutureMigrations) { + this.ignoreFutureMigrations = ignoreFutureMigrations; + } + + /** + * Whether to validate migrations and callbacks whose scripts do not obey the correct naming convention. A failure can be + * useful to check that errors such as case sensitivity in migration prefixes have been corrected. + * + * @param validateMigrationNaming {@code false} to continue normally, {@code true} to fail + * fast with an exception. (default: {@code false}) + */ + public void setValidateMigrationNaming(boolean validateMigrationNaming) { + this.validateMigrationNaming = validateMigrationNaming; + } + + /** + * Whether to automatically call validate or not when running migrate. + * + * @param validateOnMigrate {@code true} if validate should be called. {@code false} if not. (default: {@code true}) + */ + public void setValidateOnMigrate(boolean validateOnMigrate) { + this.validateOnMigrate = validateOnMigrate; + } + + /** + * Whether to automatically call clean or not when a validation error occurs. + *

This is exclusively intended as a convenience for development. even though we + * strongly recommend not to change migration scripts once they have been checked into SCM and run, this provides a + * way of dealing with this case in a smooth manner. The database will be wiped clean automatically, ensuring that + * the next migration will bring you back to the state checked into SCM.

+ *

Warning ! Do not enable in production !

+ * + * @param cleanOnValidationError {@code true} if clean should be called. {@code false} if not. (default: {@code false}) + */ + public void setCleanOnValidationError(boolean cleanOnValidationError) { + this.cleanOnValidationError = cleanOnValidationError; + } + + /** + * Whether to disable clean. + *

This is especially useful for production environments where running clean can be quite a career limiting move.

+ * + * @param cleanDisabled {@code true} to disable clean. {@code false} to leave it enabled. (default: {@code false}) + */ + public void setCleanDisabled(boolean cleanDisabled) { + this.cleanDisabled = cleanDisabled; + } + + /** + * Sets the locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both SQL and Java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem, may only + * contain SQL migrations and are only scanned recursively down non-hidden directories.

+ * + * @param locations Locations to scan recursively for migrations. (default: db/migration) + */ + public void setLocationsAsStrings(String... locations) { + this.locations = new Locations(locations); + } + + /** + * Sets the locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both SQL and Java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem, may only + * contain SQL migrations and are only scanned recursively down non-hidden directories.

+ * + * @param locations Locations to scan recursively for migrations. (default: db/migration) + */ + public void setLocations(Location... locations) { + this.locations = new Locations(Arrays.asList(locations)); + } + + /** + * Sets the encoding of Sql migrations. + * + * @param encoding The encoding of Sql migrations. (default: UTF-8) + */ + public void setEncoding(Charset encoding) { + this.encoding = encoding; + } + + /** + * Sets the encoding of Sql migrations. + * + * @param encoding The encoding of Sql migrations. (default: UTF-8) + */ + public void setEncodingAsString(String encoding) { + this.encoding = Charset.forName(encoding); + } + + /** + * Sets the default schema managed by Flyway. This schema name is case-sensitive. If not specified, but + * Schemas is, Flyway uses the first schema in that list. If that is also not specified, Flyway uses the default + * schema for the database connection. + *

Consequences:

+ *
    + *
  • This schema will be the one containing the schema history table.
  • + *
  • This schema will be the default for the database connection (provided the database supports this concept).
  • + *
+ * + * @param schema The default schema managed by Flyway. + */ + public void setDefaultSchema(String schema) { + this.defaultSchemaName = schema; + } + + /** + * Sets the schemas managed by Flyway. These schema names are case-sensitive. If not specified, Flyway uses + * the default schema for the database connection. If defaultSchema is not specified, then the first of + * this list also acts as default schema. + *

Consequences:

+ *
    + *
  • Flyway will automatically attempt to create all these schemas, unless they already exist.
  • + *
  • The schemas will be cleaned in the order of this list.
  • + *
  • If Flyway created them, the schemas themselves will be dropped when cleaning.
  • + *
+ * + * @param schemas The schemas managed by Flyway. May not be {@code null}. Must contain at least one element. + */ + public void setSchemas(String... schemas) { + this.schemaNames = schemas; + } + + /** + *

Sets the name of the schema history table that will be used by Flyway.

By default (single-schema mode) + * the schema history table is placed in the default schema for the connection provided by the datasource.

When + * the flyway.schemas property is set (multi-schema mode), the schema history table is placed in the first schema + * of the list.

+ * + * @param table The name of the schema history table that will be used by Flyway. (default: flyway_schema_history) + */ + public void setTable(String table) { + this.table = table; + } + + /** + *

Sets the tablespace where to create the schema history table that will be used by Flyway.

+ *

If not specified, Flyway uses the default tablespace for the database connection.This setting is only relevant + * for databases that do support the notion of tablespaces. Its value is simply + * ignored for all others.

+ * + * @param tablespace The tablespace where to create the schema history table that will be used by Flyway. + */ + public void setTablespace(String tablespace) { + this.tablespace = tablespace; + } + + /** + * Sets the target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + */ + public void setTarget(MigrationVersion target) { + this.target = target; + } + + /** + * Sets the target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + */ + public void setTargetAsString(String target) { + this.target = MigrationVersion.fromVersion(target); + } + + /** + * Sets whether placeholders should be replaced. + * + * @param placeholderReplacement Whether placeholders should be replaced. (default: true) + */ + public void setPlaceholderReplacement(boolean placeholderReplacement) { + this.placeholderReplacement = placeholderReplacement; + } + + /** + * Sets the placeholders to replace in sql migration scripts. + * + * @param placeholders The map of <placeholder, replacementValue> to apply to sql migration scripts. + */ + public void setPlaceholders(Map placeholders) { + this.placeholders = placeholders; + } + + /** + * Sets the prefix of every placeholder. + * + * @param placeholderPrefix The prefix of every placeholder. (default: ${ ) + */ + public void setPlaceholderPrefix(String placeholderPrefix) { + if (!StringUtils.hasLength(placeholderPrefix)) { + throw new FlywayException("placeholderPrefix cannot be empty!", ErrorCode.CONFIGURATION); + } + this.placeholderPrefix = placeholderPrefix; + } + + /** + * Sets the suffix of every placeholder. + * + * @param placeholderSuffix The suffix of every placeholder. (default: } ) + */ + public void setPlaceholderSuffix(String placeholderSuffix) { + if (!StringUtils.hasLength(placeholderSuffix)) { + throw new FlywayException("placeholderSuffix cannot be empty!", ErrorCode.CONFIGURATION); + } + this.placeholderSuffix = placeholderSuffix; + } + + /** + * Sets the file name prefix for sql migrations. + *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ * + * @param sqlMigrationPrefix The file name prefix for sql migrations (default: V) + */ + public void setSqlMigrationPrefix(String sqlMigrationPrefix) { + this.sqlMigrationPrefix = sqlMigrationPrefix; + } + + @Override + public String getUndoSqlMigrationPrefix() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("undoSqlMigrationPrefix"); + + + + + } + + /** + * Sets the file name prefix for undo SQL migrations. (default: U) + *

Undo SQL migrations are responsible for undoing the effects of the versioned migration with the same version.

+ *

They have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to U1.1__My_description.sql

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @param undoSqlMigrationPrefix The file name prefix for undo SQL migrations. (default: U) + */ + public void setUndoSqlMigrationPrefix(String undoSqlMigrationPrefix) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("undoSqlMigrationPrefix"); + + + + + } + + /** + * The manually added Java-based migrations. These are not Java-based migrations discovered through classpath + * scanning and instantiated by Flyway. Instead these are manually added instances of JavaMigration. + * This is particularly useful when working with a dependency injection container, where you may want the DI + * container to instantiate the class and wire up its dependencies for you. + * + * @param javaMigrations The manually added Java-based migrations. An empty array if none. (default: none) + */ + public void setJavaMigrations(JavaMigration... javaMigrations) { + if (javaMigrations == null) { + throw new FlywayException("javaMigrations cannot be null", ErrorCode.CONFIGURATION); + } + this.javaMigrations = javaMigrations; + } + + @Override + public boolean isStream() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("stream"); + + + + + } + + /** + * Whether to stream SQL migrations when executing them. Streaming doesn't load the entire migration in memory at + * once. Instead each statement is loaded individually. This is particularly useful for very large SQL migrations + * composed of multiple MB or even GB of reference data, as this dramatically reduces Flyway's memory consumption. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param stream {@code true} to stream SQL migrations. {@code false} to fully loaded them in memory instead. (default: {@code false}) + */ + public void setStream(boolean stream) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("stream"); + + + + + } + + @Override + public boolean isBatch() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("batch"); + + + + + } + + /** + * Whether to batch SQL statements when executing them. Batching can save up to 99 percent of network roundtrips by + * sending up to 100 statements at once over the network to the database, instead of sending each statement + * individually. This is particularly useful for very large SQL migrations composed of multiple MB or even GB of + * reference data, as this can dramatically reduce the network overhead. This is supported for INSERT, UPDATE, + * DELETE, MERGE and UPSERT statements. All other statements are automatically executed without batching. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param batch {@code true} to batch SQL statements. {@code false} to execute them individually instead. (default: {@code false}) + */ + public void setBatch(boolean batch) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("batch"); + + + + + } + + /** + * Sets the file name prefix for repeatable sql migrations. + *

Repeatable sql migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , + * which using the defaults translates to R__My_description.sql

+ * + * @param repeatableSqlMigrationPrefix The file name prefix for repeatable sql migrations (default: R) + */ + public void setRepeatableSqlMigrationPrefix(String repeatableSqlMigrationPrefix) { + this.repeatableSqlMigrationPrefix = repeatableSqlMigrationPrefix; + } + + /** + * Sets the file name separator for sql migrations. + *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ * + * @param sqlMigrationSeparator The file name separator for sql migrations (default: __) + */ + public void setSqlMigrationSeparator(String sqlMigrationSeparator) { + if (!StringUtils.hasLength(sqlMigrationSeparator)) { + throw new FlywayException("sqlMigrationSeparator cannot be empty!", ErrorCode.CONFIGURATION); + } + + this.sqlMigrationSeparator = sqlMigrationSeparator; + } + + /** + * The file name suffixes for SQL migrations. (default: .sql) + *

SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ *

Multiple suffixes (like .sql,.pkg,.pkb) can be specified for easier compatibility with other tools such as + * editors with specific file associations.

+ * + * @param sqlMigrationSuffixes The file name suffixes for SQL migrations. + */ + public void setSqlMigrationSuffixes(String... sqlMigrationSuffixes) { + this.sqlMigrationSuffixes = sqlMigrationSuffixes; + } + + /** + * Sets the datasource to use. Must have the necessary privileges to execute ddl. + * + * @param dataSource The datasource to use. Must have the necessary privileges to execute ddl. + */ + public void setDataSource(DataSource dataSource) { + driver = null; + url = null; + user = null; + password = null; + this.dataSource = dataSource; + } + + /** + * Sets the datasource to use. Must have the necessary privileges to execute ddl. + *

To use a custom ClassLoader, setClassLoader() must be called prior to calling this method.

+ * + * @param url The JDBC URL of the database. + * @param user The user of the database. + * @param password The password of the database. + */ + public void setDataSource(String url, String user, String password) { + this.dataSource = new DriverDataSource(classLoader, null, url, user, password); + } + + /** + * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will + * wait 1 second before attempting to connect again, up to the maximum number of times specified by connectRetries. + * + * @param connectRetries The maximum number of retries (default: 0). + */ + public void setConnectRetries(int connectRetries) { + if (connectRetries < 0) { + throw new FlywayException("Invalid number of connectRetries (must be 0 or greater): " + connectRetries, ErrorCode.CONFIGURATION); + } + this.connectRetries = connectRetries; + } + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * + * @param initSql The SQL statements. (default: {@code null}) + */ + public void setInitSql(String initSql) { + this.initSql = initSql; + } + + /** + * Sets the version to tag an existing schema with when executing baseline. + * + * @param baselineVersion The version to tag an existing schema with when executing baseline. (default: 1) + */ + public void setBaselineVersion(MigrationVersion baselineVersion) { + this.baselineVersion = baselineVersion; + } + + /** + * Sets the version to tag an existing schema with when executing baseline. + * + * @param baselineVersion The version to tag an existing schema with when executing baseline. (default: 1) + */ + public void setBaselineVersionAsString(String baselineVersion) { + this.baselineVersion = MigrationVersion.fromVersion(baselineVersion); + } + + /** + * Sets the description to tag an existing schema with when executing baseline. + * + * @param baselineDescription The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + public void setBaselineDescription(String baselineDescription) { + this.baselineDescription = baselineDescription; + } + + /** + *

+ * Whether to automatically call baseline when migrate is executed against a non-empty schema with no schema history table. + * This schema will then be baselined with the {@code baselineVersion} before executing the migrations. + * Only migrations above {@code baselineVersion} will then be applied. + *

+ *

+ * This is useful for initial Flyway production deployments on projects with an existing DB. + *

+ *

+ * Be careful when enabling this as it removes the safety net that ensures + * Flyway does not migrate the wrong database in case of a configuration mistake! + *

+ * + * @param baselineOnMigrate {@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false}) + */ + public void setBaselineOnMigrate(boolean baselineOnMigrate) { + this.baselineOnMigrate = baselineOnMigrate; + } + + /** + * Allows migrations to be run "out of order". + *

If you already have versions 1 and 3 applied, and now a version 2 is found, + * it will be applied too instead of being ignored.

+ * + * @param outOfOrder {@code true} if outOfOrder migrations should be applied, {@code false} if not. (default: {@code false}) + */ + public void setOutOfOrder(boolean outOfOrder) { + this.outOfOrder = outOfOrder; + } + + /** + * Gets the callbacks for lifecycle notifications. + * + * @return The callbacks for lifecycle notifications. An empty array if none. (default: none) + */ + @Override + public Callback[] getCallbacks() { + return callbacks.toArray(new Callback[0]); + } + + @Override + public boolean isSkipDefaultCallbacks() { + return skipDefaultCallbacks; + } + + /** + * Set the callbacks for lifecycle notifications. + * + * @param callbacks The callbacks for lifecycle notifications. (default: none) + */ + public void setCallbacks(Callback... callbacks) { + this.callbacks.clear(); + this.callbacks.addAll(Arrays.asList(callbacks)); + } + + /** + * Set the callbacks for lifecycle notifications. + * + * @param callbacks The fully qualified class names of the callbacks for lifecycle notifications. (default: none) + */ + public void setCallbacksAsClassNames(String... callbacks) { + this.callbacks.clear(); + for (String callback : callbacks) { + Object o = ClassUtils.instantiate(callback, classLoader); + if (o instanceof Callback) { + this.callbacks.add((Callback) o); + } else { + throw new FlywayException("Invalid callback: " + callback + " (must implement org.flywaydb.core.api.callback.Callback)", ErrorCode.CONFIGURATION); + } + } + } + + /** + * Whether Flyway should skip the default callbacks. If true, only custom callbacks are used. + * + * @param skipDefaultCallbacks Whether default built-in callbacks should be skipped.

(default: false)

+ */ + public void setSkipDefaultCallbacks(boolean skipDefaultCallbacks) { + this.skipDefaultCallbacks = skipDefaultCallbacks; + } + + /** + * Sets custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + * + * @param resolvers The custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. (default: empty list) + */ + public void setResolvers(MigrationResolver... resolvers) { + this.resolvers = resolvers; + } + + /** + * Sets custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + * + * @param resolvers The fully qualified class names of the custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. (default: empty list) + */ + public void setResolversAsClassNames(String... resolvers) { + List resolverList = ClassUtils.instantiateAll(resolvers, classLoader); + setResolvers(resolverList.toArray(new MigrationResolver[resolvers.length])); + } + + /** + * Whether Flyway should skip the default resolvers. If true, only custom resolvers are used. + * + * @param skipDefaultResolvers Whether default built-in resolvers should be skipped.

(default: false)

+ */ + public void setSkipDefaultResolvers(boolean skipDefaultResolvers) { + this.skipDefaultResolvers = skipDefaultResolvers; + } + + + + + + + + + + + + + + + + + + + @Override + public boolean isOracleSqlplus() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("oracle.sqlplus"); + + + + + } + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param oracleSqlplus {@code true} to active SQL*Plus support. {@code false} to fail fast instead. (default: {@code false}) + */ + public void setOracleSqlplus(boolean oracleSqlplus) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("oracle.sqlplus"); + + + + + } + + @Override + public boolean isOracleSqlplusWarn() { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("oracle.sqlplusWarn"); + + + + + } + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @param oracleSqlplusWarn {@code true} to issue a warning. {@code false} to fail fast instead. (default: {@code false}) + */ + public void setOracleSqlplusWarn(boolean oracleSqlplusWarn) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("oracle.sqlplusWarn"); + + + + + } + + /** + * Your Flyway license key (FL01...). Not yet a Flyway Pro or Enterprise Edition customer? + * Request your Flyway trial license key + * to try out Flyway Pro and Enterprise Edition features free for 30 days. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @param licenseKey Your Flyway license key. + */ + public void setLicenseKey(String licenseKey) { + + LOG.warn(Edition.PRO + " or " + Edition.ENTERPRISE + " upgrade required: " + licenseKey + + " is not supported by " + Edition.COMMUNITY + "."); + + + + + } + + /** + * Configure with the same values as this existing configuration. + * + * @param configuration The configuration to use. + */ + public void configure(Configuration configuration) { + setBaselineDescription(configuration.getBaselineDescription()); + setBaselineOnMigrate(configuration.isBaselineOnMigrate()); + setBaselineVersion(configuration.getBaselineVersion()); + setCallbacks(configuration.getCallbacks()); + setCleanDisabled(configuration.isCleanDisabled()); + setCleanOnValidationError(configuration.isCleanOnValidationError()); + setDataSource(configuration.getDataSource()); + setConnectRetries(configuration.getConnectRetries()); + setInitSql(configuration.getInitSql()); + + + + + + + + + + + + setEncoding(configuration.getEncoding()); + setGroup(configuration.isGroup()); + setValidateMigrationNaming(configuration.isValidateMigrationNaming()); + setIgnoreFutureMigrations(configuration.isIgnoreFutureMigrations()); + setIgnoreMissingMigrations(configuration.isIgnoreMissingMigrations()); + setIgnoreIgnoredMigrations(configuration.isIgnoreIgnoredMigrations()); + setIgnorePendingMigrations(configuration.isIgnorePendingMigrations()); + setInstalledBy(configuration.getInstalledBy()); + setJavaMigrations(configuration.getJavaMigrations()); + setLocations(configuration.getLocations()); + setMixed(configuration.isMixed()); + setOutOfOrder(configuration.isOutOfOrder()); + setPlaceholderPrefix(configuration.getPlaceholderPrefix()); + setPlaceholderReplacement(configuration.isPlaceholderReplacement()); + setPlaceholders(configuration.getPlaceholders()); + setPlaceholderSuffix(configuration.getPlaceholderSuffix()); + setRepeatableSqlMigrationPrefix(configuration.getRepeatableSqlMigrationPrefix()); + setResolvers(configuration.getResolvers()); + setDefaultSchema(configuration.getDefaultSchema()); + setSchemas(configuration.getSchemas()); + setSkipDefaultCallbacks(configuration.isSkipDefaultCallbacks()); + setSkipDefaultResolvers(configuration.isSkipDefaultResolvers()); + setSqlMigrationPrefix(configuration.getSqlMigrationPrefix()); + setSqlMigrationSeparator(configuration.getSqlMigrationSeparator()); + setSqlMigrationSuffixes(configuration.getSqlMigrationSuffixes()); + setTable(configuration.getTable()); + setTablespace(configuration.getTablespace()); + setTarget(configuration.getTarget()); + setValidateOnMigrate(configuration.isValidateOnMigrate()); + } + + /** + * Whether Flyway should output a table with the results of queries when executing migrations. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to output the results table (default: {@code true}) + */ + private void setOutputQueryResults(boolean outputQueryResults) { + + throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("outputQueryResults"); + + + + + } + + /** + * Configures Flyway with these properties. This overwrites any existing configuration. Property names are + * documented in the flyway maven plugin. + *

To use a custom ClassLoader, setClassLoader() must be called prior to calling this method.

+ * + * @param properties Properties used for configuration. + * @throws FlywayException when the configuration failed. + */ + public void configure(Properties properties) { + configure(ConfigUtils.propertiesToMap(properties)); + } + + /** + * Configures Flyway with these properties. This overwrites any existing configuration. Property names are + * documented in the flyway maven plugin. + *

To use a custom ClassLoader, it must be passed to the Flyway constructor prior to calling this method.

+ * + * @param props Properties used for configuration. + * @throws FlywayException when the configuration failed. + */ + public void configure(Map props) { + // Make copy to prevent removing elements from the original. + props = new HashMap<>(props); + + String driverProp = props.remove(ConfigUtils.DRIVER); + if (driverProp != null) { + dataSource = null; + driver = driverProp; + } + String urlProp = props.remove(ConfigUtils.URL); + if (urlProp != null) { + dataSource = null; + url = urlProp; + } + String userProp = props.remove(ConfigUtils.USER); + if (userProp != null) { + dataSource = null; + user = userProp; + } + String passwordProp = props.remove(ConfigUtils.PASSWORD); + if (passwordProp != null) { + dataSource = null; + password = passwordProp; + } + if (StringUtils.hasText(url) && (StringUtils.hasText(urlProp) || + StringUtils.hasText(driverProp) || StringUtils.hasText(userProp) || StringUtils.hasText(passwordProp))) { + setDataSource(new DriverDataSource(classLoader, driver, url, user, password)); + } + Integer connectRetriesProp = removeInteger(props, ConfigUtils.CONNECT_RETRIES); + if (connectRetriesProp != null) { + setConnectRetries(connectRetriesProp); + } + String initSqlProp = props.remove(ConfigUtils.INIT_SQL); + if (initSqlProp != null) { + setInitSql(initSqlProp); + } + String locationsProp = props.remove(ConfigUtils.LOCATIONS); + if (locationsProp != null) { + setLocationsAsStrings(StringUtils.tokenizeToStringArray(locationsProp, ",")); + } + Boolean placeholderReplacementProp = removeBoolean(props, ConfigUtils.PLACEHOLDER_REPLACEMENT); + if (placeholderReplacementProp != null) { + setPlaceholderReplacement(placeholderReplacementProp); + } + String placeholderPrefixProp = props.remove(ConfigUtils.PLACEHOLDER_PREFIX); + if (placeholderPrefixProp != null) { + setPlaceholderPrefix(placeholderPrefixProp); + } + String placeholderSuffixProp = props.remove(ConfigUtils.PLACEHOLDER_SUFFIX); + if (placeholderSuffixProp != null) { + setPlaceholderSuffix(placeholderSuffixProp); + } + String sqlMigrationPrefixProp = props.remove(ConfigUtils.SQL_MIGRATION_PREFIX); + if (sqlMigrationPrefixProp != null) { + setSqlMigrationPrefix(sqlMigrationPrefixProp); + } + String undoSqlMigrationPrefixProp = props.remove(ConfigUtils.UNDO_SQL_MIGRATION_PREFIX); + if (undoSqlMigrationPrefixProp != null) { + setUndoSqlMigrationPrefix(undoSqlMigrationPrefixProp); + } + String repeatableSqlMigrationPrefixProp = props.remove(ConfigUtils.REPEATABLE_SQL_MIGRATION_PREFIX); + if (repeatableSqlMigrationPrefixProp != null) { + setRepeatableSqlMigrationPrefix(repeatableSqlMigrationPrefixProp); + } + String sqlMigrationSeparatorProp = props.remove(ConfigUtils.SQL_MIGRATION_SEPARATOR); + if (sqlMigrationSeparatorProp != null) { + setSqlMigrationSeparator(sqlMigrationSeparatorProp); + } + String sqlMigrationSuffixesProp = props.remove(ConfigUtils.SQL_MIGRATION_SUFFIXES); + if (sqlMigrationSuffixesProp != null) { + setSqlMigrationSuffixes(StringUtils.tokenizeToStringArray(sqlMigrationSuffixesProp, ",")); + } + String encodingProp = props.remove(ConfigUtils.ENCODING); + if (encodingProp != null) { + setEncodingAsString(encodingProp); + } + String defaultSchemaProp = props.remove(ConfigUtils.DEFAULT_SCHEMA); + if (defaultSchemaProp != null) { + setDefaultSchema(defaultSchemaProp); + } + String schemasProp = props.remove(ConfigUtils.SCHEMAS); + if (schemasProp != null) { + setSchemas(StringUtils.tokenizeToStringArray(schemasProp, ",")); + } + String tableProp = props.remove(ConfigUtils.TABLE); + if (tableProp != null) { + setTable(tableProp); + } + String tablespaceProp = props.remove(ConfigUtils.TABLESPACE); + if (tablespaceProp != null) { + setTablespace(tablespaceProp); + } + Boolean cleanOnValidationErrorProp = removeBoolean(props, ConfigUtils.CLEAN_ON_VALIDATION_ERROR); + if (cleanOnValidationErrorProp != null) { + setCleanOnValidationError(cleanOnValidationErrorProp); + } + Boolean cleanDisabledProp = removeBoolean(props, ConfigUtils.CLEAN_DISABLED); + if (cleanDisabledProp != null) { + setCleanDisabled(cleanDisabledProp); + } + Boolean validateOnMigrateProp = removeBoolean(props, ConfigUtils.VALIDATE_ON_MIGRATE); + if (validateOnMigrateProp != null) { + setValidateOnMigrate(validateOnMigrateProp); + } + String baselineVersionProp = props.remove(ConfigUtils.BASELINE_VERSION); + if (baselineVersionProp != null) { + setBaselineVersion(MigrationVersion.fromVersion(baselineVersionProp)); + } + String baselineDescriptionProp = props.remove(ConfigUtils.BASELINE_DESCRIPTION); + if (baselineDescriptionProp != null) { + setBaselineDescription(baselineDescriptionProp); + } + Boolean baselineOnMigrateProp = removeBoolean(props, ConfigUtils.BASELINE_ON_MIGRATE); + if (baselineOnMigrateProp != null) { + setBaselineOnMigrate(baselineOnMigrateProp); + } + Boolean ignoreMissingMigrationsProp = removeBoolean(props, ConfigUtils.IGNORE_MISSING_MIGRATIONS); + if (ignoreMissingMigrationsProp != null) { + setIgnoreMissingMigrations(ignoreMissingMigrationsProp); + } + Boolean ignoreIgnoredMigrationsProp = removeBoolean(props, ConfigUtils.IGNORE_IGNORED_MIGRATIONS); + if (ignoreIgnoredMigrationsProp != null) { + setIgnoreIgnoredMigrations(ignoreIgnoredMigrationsProp); + } + Boolean ignorePendingMigrationsProp = removeBoolean(props, ConfigUtils.IGNORE_PENDING_MIGRATIONS); + if (ignorePendingMigrationsProp != null) { + setIgnorePendingMigrations(ignorePendingMigrationsProp); + } + Boolean ignoreFutureMigrationsProp = removeBoolean(props, ConfigUtils.IGNORE_FUTURE_MIGRATIONS); + if (ignoreFutureMigrationsProp != null) { + setIgnoreFutureMigrations(ignoreFutureMigrationsProp); + } + Boolean validateMigrationNamingProp = removeBoolean(props, ConfigUtils.VALIDATE_MIGRATION_NAMING); + if (validateMigrationNamingProp != null) { + setValidateMigrationNaming(validateMigrationNamingProp); + } + String targetProp = props.remove(ConfigUtils.TARGET); + if (targetProp != null) { + setTarget(MigrationVersion.fromVersion(targetProp)); + } + Boolean outOfOrderProp = removeBoolean(props, ConfigUtils.OUT_OF_ORDER); + if (outOfOrderProp != null) { + setOutOfOrder(outOfOrderProp); + } + Boolean outputQueryResultsProp = removeBoolean(props, ConfigUtils.OUTPUT_QUERY_RESULTS); + if (outputQueryResultsProp != null) { + setOutputQueryResults(outputQueryResultsProp); + } + String resolversProp = props.remove(ConfigUtils.RESOLVERS); + if (StringUtils.hasLength(resolversProp)) { + setResolversAsClassNames(StringUtils.tokenizeToStringArray(resolversProp, ",")); + } + Boolean skipDefaultResolversProp = removeBoolean(props, ConfigUtils.SKIP_DEFAULT_RESOLVERS); + if (skipDefaultResolversProp != null) { + setSkipDefaultResolvers(skipDefaultResolversProp); + } + String callbacksProp = props.remove(ConfigUtils.CALLBACKS); + if (StringUtils.hasLength(callbacksProp)) { + setCallbacksAsClassNames(StringUtils.tokenizeToStringArray(callbacksProp, ",")); + } + Boolean skipDefaultCallbacksProp = removeBoolean(props, ConfigUtils.SKIP_DEFAULT_CALLBACKS); + if (skipDefaultCallbacksProp != null) { + setSkipDefaultCallbacks(skipDefaultCallbacksProp); + } + + Map placeholdersFromProps = new HashMap<>(getPlaceholders()); + Iterator> iterator = props.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + String propertyName = entry.getKey(); + + if (propertyName.startsWith(ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX) + && propertyName.length() > ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX.length()) { + String placeholderName = propertyName.substring(ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX.length()); + String placeholderValue = entry.getValue(); + placeholdersFromProps.put(placeholderName, placeholderValue); + iterator.remove(); + } + } + setPlaceholders(placeholdersFromProps); + + Boolean mixedProp = removeBoolean(props, ConfigUtils.MIXED); + if (mixedProp != null) { + setMixed(mixedProp); + } + + Boolean groupProp = removeBoolean(props, ConfigUtils.GROUP); + if (groupProp != null) { + setGroup(groupProp); + } + + String installedByProp = props.remove(ConfigUtils.INSTALLED_BY); + if (installedByProp != null) { + setInstalledBy(installedByProp); + } + + String dryRunOutputProp = props.remove(ConfigUtils.DRYRUN_OUTPUT); + if (dryRunOutputProp != null) { + setDryRunOutputAsFileName(dryRunOutputProp); + } + + String errorOverridesProp = props.remove(ConfigUtils.ERROR_OVERRIDES); + if (errorOverridesProp != null) { + setErrorOverrides(StringUtils.tokenizeToStringArray(errorOverridesProp, ",")); + } + + Boolean streamProp = removeBoolean(props, ConfigUtils.STREAM); + if (streamProp != null) { + setStream(streamProp); + } + + Boolean batchProp = removeBoolean(props, ConfigUtils.BATCH); + if (batchProp != null) { + setBatch(batchProp); + } + + Boolean oracleSqlplusProp = removeBoolean(props, ConfigUtils.ORACLE_SQLPLUS); + if (oracleSqlplusProp != null) { + setOracleSqlplus(oracleSqlplusProp); + } + + Boolean oracleSqlplusWarnProp = removeBoolean(props, ConfigUtils.ORACLE_SQLPLUS_WARN); + if (oracleSqlplusWarnProp != null) { + setOracleSqlplusWarn(oracleSqlplusWarnProp); + } + + String licenseKeyProp = props.remove(ConfigUtils.LICENSE_KEY); + if (licenseKeyProp != null) { + setLicenseKey(licenseKeyProp); + } + + ConfigUtils.checkConfigurationForUnrecognisedProperties(props, "flyway."); + } + + /** + * Configures Flyway using FLYWAY_* environment variables. + */ + public void configureUsingEnvVars() { + configure(ConfigUtils.environmentVariablesToPropertyMap()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java new file mode 100644 index 00000000..4869c684 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java @@ -0,0 +1,518 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.configuration; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.MigrationResolver; + +import javax.sql.DataSource; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * Flyway configuration. + */ +public interface Configuration { + /** + * Retrieves the ClassLoader to use for loading migrations, resolvers, etc from the classpath. + * + * @return The ClassLoader to use for loading migrations, resolvers, etc from the classpath. + * (default: Thread.currentThread().getContextClassLoader() ) + */ + ClassLoader getClassLoader(); + + /** + * Retrieves the dataSource to use to access the database. Must have the necessary privileges to execute ddl. + * + * @return The dataSource to use to access the database. Must have the necessary privileges to execute ddl. + */ + DataSource getDataSource(); + + /** + * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will + * wait 1 second before attempting to connect again, up to the maximum number of times specified by connectRetries. + * + * @return The maximum number of retries when attempting to connect to the database. (default: 0) + */ + int getConnectRetries(); + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * + * @return The SQL statements. (default: {@code null}) + */ + String getInitSql(); + + /** + * Retrieves the version to tag an existing schema with when executing baseline. + * + * @return The version to tag an existing schema with when executing baseline. (default: 1) + */ + MigrationVersion getBaselineVersion(); + + /** + * Retrieves the description to tag an existing schema with when executing baseline. + * + * @return The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + String getBaselineDescription(); + + /** + * Retrieves the custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + * + * @return The custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. An empty array if none. + * (default: none) + */ + MigrationResolver[] getResolvers(); + + /** + * Whether Flyway should skip the default resolvers. If true, only custom resolvers are used. + * + * @return Whether default built-in resolvers should be skipped. (default: false) + */ + boolean isSkipDefaultResolvers(); + + /** + * Gets the callbacks for lifecycle notifications. + * + * @return The callbacks for lifecycle notifications. An empty array if none. (default: none) + */ + Callback[] getCallbacks(); + + /** + * Whether Flyway should skip the default callbacks. If true, only custom callbacks are used. + * + * @return Whether default built-in callbacks should be skipped. (default: false) + */ + boolean isSkipDefaultCallbacks(); + + /** + * The file name prefix for versioned SQL migrations. + *

Versioned SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1.1__My_description.sql

+ * + * @return The file name prefix for sql migrations. (default: V) + */ + String getSqlMigrationPrefix(); + + /** + * The file name prefix for undo SQL migrations. + *

Undo SQL migrations are responsible for undoing the effects of the versioned migration with the same version.

+ *

They have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to U1.1__My_description.sql

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @return The file name prefix for undo sql migrations. (default: U) + */ + String getUndoSqlMigrationPrefix(); + + /** + * Retrieves the file name prefix for repeatable SQL migrations. + *

Repeatable SQL migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , + * which using the defaults translates to R__My_description.sql

+ * + * @return The file name prefix for repeatable sql migrations. (default: R) + */ + String getRepeatableSqlMigrationPrefix(); + + /** + * Retrieves the file name separator for sql migrations. + *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ * + * @return The file name separator for sql migrations. (default: __) + */ + String getSqlMigrationSeparator(); + + /** + * The file name suffixes for SQL migrations. (default: .sql) + *

SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ *

Multiple suffixes (like .sql,.pkg,.pkb) can be specified for easier compatibility with other tools such as + * editors with specific file associations.

+ * + * @return The file name suffixes for SQL migrations. + */ + String[] getSqlMigrationSuffixes(); + + /** + * The manually added Java-based migrations. These are not Java-based migrations discovered through classpath + * scanning and instantiated by Flyway. Instead these are manually added instances of JavaMigration. + * This is particularly useful when working with a dependency injection container, where you may want the DI + * container to instantiate the class and wire up its dependencies for you. + * + * @return The manually added Java-based migrations. An empty array if none. (default: none) + */ + JavaMigration[] getJavaMigrations(); + + /** + * Checks whether placeholders should be replaced. + * + * @return Whether placeholders should be replaced. (default: true) + */ + boolean isPlaceholderReplacement(); + + /** + * Retrieves the suffix of every placeholder. + * + * @return The suffix of every placeholder. (default: } ) + */ + String getPlaceholderSuffix(); + + /** + * Retrieves the prefix of every placeholder. + * + * @return The prefix of every placeholder. (default: ${ ) + */ + String getPlaceholderPrefix(); + + /** + * Retrieves the map of <placeholder, replacementValue> to apply to sql migration scripts. + * + * @return The map of <placeholder, replacementValue> to apply to sql migration scripts. + */ + Map getPlaceholders(); + + /** + * Gets the target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + * @return The target version up to which Flyway should consider migrations. Defaults to {@code latest} + */ + MigrationVersion getTarget(); + + /** + *

Retrieves the name of the schema history table that will be used by Flyway.

By default (single-schema + * mode) the schema history table is placed in the default schema for the connection provided by the datasource.

+ * When the flyway.schemas property is set (multi-schema mode), the schema history table is placed in the first + * schema of the list.

+ * + * @return The name of the schema history table that will be used by Flyway. (default: flyway_schema_history) + */ + String getTable(); + + /** + *

The tablespace where to create the schema history table that will be used by Flyway.

+ *

If not specified, Flyway uses the default tablespace for the database connection. + * This setting is only relevant for databases that do support the notion of tablespaces. Its value is simply + * ignored for all others.

+ * + * @return The tablespace where to create the schema history table that will be used by Flyway. + */ + String getTablespace(); + + /** + * The default schema managed by Flyway. This schema name is case-sensitive. If not specified, but schemas + * is, Flyway uses the first schema in that list. If that is also not specified, Flyway uses the default schema for the + * database connection. + *

Consequences:

+ *
    + *
  • This schema will be the one containing the schema history table.
  • + *
  • This schema will be the default for the database connection (provided the database supports this concept).
  • + *
+ * + * @return The schemas managed by Flyway. (default: The first schema specified in getSchemas(), and failing that + * the default schema for the database connection) + */ + String getDefaultSchema(); + + /** + * The schemas managed by Flyway. These schema names are case-sensitive. If not specified, Flyway uses + * the default schema for the database connection. If defaultSchemaName is not specified, then the first of + * this list also acts as default schema. + *

Consequences:

+ *
    + *
  • Flyway will automatically attempt to create all these schemas, unless they already exist.
  • + *
  • The schemas will be cleaned in the order of this list.
  • + *
  • If Flyway created them, the schemas themselves will be dropped when cleaning.
  • + *
+ * + * @return The schemas managed by Flyway. (default: The default schema for the database connection) + */ + String[] getSchemas(); + + /** + * Retrieves the encoding of Sql migrations. + * + * @return The encoding of Sql migrations. (default: UTF-8) + */ + Charset getEncoding(); + + /** + * Retrieves the locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both SQL and Java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem, may only + * contain SQL migrations and are only scanned recursively down non-hidden directories.

+ * + * @return Locations to scan recursively for migrations. (default: classpath:db/migration) + */ + Location[] getLocations(); + + /** + *

+ * Whether to automatically call baseline when migrate is executed against a non-empty schema with no schema history table. + * This schema will then be initialized with the {@code baselineVersion} before executing the migrations. + * Only migrations above {@code baselineVersion} will then be applied. + *

+ *

+ * This is useful for initial Flyway production deployments on projects with an existing DB. + *

+ *

+ * Be careful when enabling this as it removes the safety net that ensures + * Flyway does not migrate the wrong database in case of a configuration mistake! + *

+ * + * @return {@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false}) + */ + boolean isBaselineOnMigrate(); + + /** + * Allows migrations to be run "out of order". + *

If you already have versions 1 and 3 applied, and now a version 2 is found, + * it will be applied too instead of being ignored.

+ * + * @return {@code true} if outOfOrder migrations should be applied, {@code false} if not. (default: {@code false}) + */ + boolean isOutOfOrder(); + + /** + * Ignore missing migrations when reading the schema history table. These are migrations that were performed by an + * older deployment of the application that are no longer available in this version. For example: we have migrations + * available on the classpath with versions 1.0 and 3.0. The schema history table indicates that a migration with version 2.0 + * (unknown to us) has also been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to deploy + * a newer version of the application even though it doesn't contain migrations included with an older one anymore. + * Note that if the most recently applied migration is removed, Flyway has no way to know it is missing and will + * mark it as future instead. + * + * @return {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + boolean isIgnoreMissingMigrations(); + + /** + * Ignore ignored migrations when reading the schema history table. These are migrations that were added in between + * already migrated migrations in this version. For example: we have migrations available on the classpath with + * versions from 1.0 to 3.0. The schema history table indicates that version 1 was finished on 1.0.15, and the next + * one was 2.0.0. But with the next release a new migration was added to version 1: 1.0.16. Such scenario is ignored + * by migrate command, but by default is rejected by validate. When ignoreIgnoredMigrations is enabled, such case + * will not be reported by validate command. This is useful for situations where one must be able to deliver + * complete set of migrations in a delivery package for multiple versions of the product, and allows for further + * development of older versions. + * + * @return {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + boolean isIgnoreIgnoredMigrations(); + + /** + * Ignore pending migrations when reading the schema history table. These are migrations that are available + * but have not yet been applied. This can be useful for verifying that in-development migration changes + * don't contain any validation-breaking changes of migrations that have already been applied to a production + * environment, e.g. as part of a CI/CD process, without failing because of the existence of new migration versions. + * + * @return {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + boolean isIgnorePendingMigrations(); + + /** + * Ignore future migrations when reading the schema history table. These are migrations that were performed by a + * newer deployment of the application that are not yet available in this version. For example: we have migrations + * available on the classpath up to version 3.0. The schema history table indicates that a migration to version 4.0 + * (unknown to us) has already been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to redeploy + * an older version of the application after the database has been migrated by a newer one. + * + * @return {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception. + * (default: {@code true}) + */ + boolean isIgnoreFutureMigrations(); + + /** + * Whether to validate migrations and callbacks whose scripts do not obey the correct naming convention. A failure can be + * useful to check that errors such as case sensitivity in migration prefixes have been corrected. + * + * @return {@code false} to continue normally, {@code true} to fail fast with an exception. (default: {@code false}) + */ + boolean isValidateMigrationNaming(); + + /** + * Whether to automatically call validate or not when running migrate. + * + * @return {@code true} if validate should be called. {@code false} if not. (default: {@code true}) + */ + boolean isValidateOnMigrate(); + + /** + * Whether to automatically call clean or not when a validation error occurs. + *

This is exclusively intended as a convenience for development. even though we + * strongly recommend not to change migration scripts once they have been checked into SCM and run, this provides a + * way of dealing with this case in a smooth manner. The database will be wiped clean automatically, ensuring that + * the next migration will bring you back to the state checked into SCM.

+ *

Warning ! Do not enable in production !

+ * + * @return {@code true} if clean should be called. {@code false} if not. (default: {@code false}) + */ + boolean isCleanOnValidationError(); + + /** + * Whether to disable clean. + *

This is especially useful for production environments where running clean can be quite a career limiting move.

+ * + * @return {@code true} to disable clean. {@code false} to leave it enabled. (default: {@code false}) + */ + boolean isCleanDisabled(); + + /** + * Whether to allow mixing transactional and non-transactional statements within the same migration. Enabling this + * automatically causes the entire affected migration to be run without a transaction. + * + *

Note that this is only applicable for PostgreSQL, Aurora PostgreSQL, SQL Server and SQLite which all have + * statements that do not run at all within a transaction.

+ *

This is not to be confused with implicit transaction, as they occur in MySQL or Oracle, where even though a + * DDL statement was run within a transaction, the database will issue an implicit commit before and after + * its execution.

+ * + * @return {@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false}) + */ + boolean isMixed(); + + /** + * Whether to group all pending migrations together in the same transaction when applying them (only recommended for databases with support for DDL transactions). + * + * @return {@code true} if migrations should be grouped. {@code false} if they should be applied individually instead. (default: {@code false}) + */ + boolean isGroup(); + + /** + * The username that will be recorded in the schema history table as having applied the migration. + * + * @return The username or {@code null} for the current database user of the connection. (default: {@code null}). + */ + String getInstalledBy(); + + /** + * Rules for the built-in error handler that let you override specific SQL states and errors codes in order to force + * specific errors or warnings to be treated as debug messages, info messages, warnings or errors. + *

Each error override has the following format: {@code STATE:12345:W}. + * It is a 5 character SQL state (or * to match all SQL states), a colon, + * the SQL error code (or * to match all SQL error codes), a colon and finally + * the desired behavior that should override the initial one.

+ *

The following behaviors are accepted:

+ *
    + *
  • {@code D} to force a debug message
  • + *
  • {@code D-} to force a debug message, but do not show the original sql state and error code
  • + *
  • {@code I} to force an info message
  • + *
  • {@code I-} to force an info message, but do not show the original sql state and error code
  • + *
  • {@code W} to force a warning
  • + *
  • {@code W-} to force a warning, but do not show the original sql state and error code
  • + *
  • {@code E} to force an error
  • + *
  • {@code E-} to force an error, but do not show the original sql state and error code
  • + *
+ *

Example 1: to force Oracle stored procedure compilation issues to produce + * errors instead of warnings, the following errorOverride can be used: {@code 99999:17110:E}

+ *

Example 2: to force SQL Server PRINT messages to be displayed as info messages (without SQL state and error + * code details) instead of warnings, the following errorOverride can be used: {@code S0001:0:I-}

+ *

Example 3: to force all errors with SQL error code 123 to be treated as warnings instead, + * the following errorOverride can be used: {@code *:123:W}

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @return The ErrorOverrides or an empty array if none are defined. (default: none) + */ + String[] getErrorOverrides(); + + /** + * The stream where to output the SQL statements of a migration dry run. {@code null} if the SQL statements + * are executed against the database directly. + *

Flyway Pro and Flyway Enterprise only

+ * + * @return The stream or {@code null} if the SQL statements are executed against the database directly. + */ + OutputStream getDryRunOutput(); + + /** + * Whether to stream SQL migrations when executing them. Streaming doesn't load the entire migration in memory at + * once. Instead each statement is loaded individually. This is particularly useful for very large SQL migrations + * composed of multiple MB or even GB of reference data, as this dramatically reduces Flyway's memory consumption. + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to stream SQL migrations. {@code false} to fully loaded them in memory instead. (default: {@code false}) + */ + boolean isStream(); + + /** + * Whether to batch SQL statements when executing them. Batching can save up to 99 percent of network roundtrips by + * sending up to 100 statements at once over the network to the database, instead of sending each statement + * individually. This is particularly useful for very large SQL migrations composed of multiple MB or even GB of + * reference data, as this can dramatically reduce the network overhead. This is supported for INSERT, UPDATE, + * DELETE, MERGE and UPSERT statements. All other statements are automatically executed without batching. + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to batch SQL statements. {@code false} to execute them individually instead. (default: {@code false}) + */ + boolean isBatch(); + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to active SQL*Plus support. {@code false} to fail fast instead. (default: {@code false}) + */ + boolean isOracleSqlplus(); + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to issue a warning. {@code false} to fail fast instead. (default: {@code false}) + */ + boolean isOracleSqlplusWarn(); + + /** + * Your Flyway license key (FL01...). Not yet a Flyway Pro or Enterprise Edition customer? + * Request your Flyway trial license key + * to try out Flyway Pro and Enterprise Edition features free for 30 days. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return Your Flyway license key. + */ + String getLicenseKey(); + + /** + * Whether Flyway should output a table with the results of queries when executing migrations. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @return {@code true} to output the results table (default: {@code true}) + */ + boolean outputQueryResults(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java new file mode 100644 index 00000000..97fcaaf4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java @@ -0,0 +1,1100 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.configuration; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.internal.util.ClassUtils; + +import javax.sql.DataSource; +import java.io.File; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Fluent configuration for Flyway. This is the preferred means of configuring the Flyway API. + *

+ * This configuration can be passed to Flyway using the new Flyway(Configuration) constructor. + *

+ */ +public class FluentConfiguration implements Configuration { + private final ClassicConfiguration config; + + /** + * Creates a new default configuration. + */ + public FluentConfiguration() { + config = new ClassicConfiguration(); + } + + /** + * Creates a new default configuration with this class loader. + * + * @param classLoader The ClassLoader to use for loading migrations, resolvers, etc from the classpath. (default: Thread.currentThread().getContextClassLoader() ) + */ + public FluentConfiguration(ClassLoader classLoader) { + config = new ClassicConfiguration(classLoader); + } + + /** + * Loads this configuration into a new Flyway instance. + * + * @return The new fully-configured Flyway instance. + */ + public Flyway load() { + return new Flyway(this); + } + + /** + * Configure with the same values as this existing configuration. + * + * @param configuration The configuration to use. + */ + public FluentConfiguration configuration(Configuration configuration) { + config.configure(configuration); + return this; + } + + @Override + public Location[] getLocations() { + return config.getLocations(); + } + + @Override + public Charset getEncoding() { + return config.getEncoding(); + } + + @Override + public String getDefaultSchema() { return config.getDefaultSchema(); } + + @Override + public String[] getSchemas() { return config.getSchemas(); } + + @Override + public String getTable() { + return config.getTable(); + } + + @Override + public String getTablespace() { + return config.getTablespace(); + } + + @Override + public MigrationVersion getTarget() { + return config.getTarget(); + } + + @Override + public boolean isPlaceholderReplacement() { + return config.isPlaceholderReplacement(); + } + + @Override + public Map getPlaceholders() { + return config.getPlaceholders(); + } + + @Override + public String getPlaceholderPrefix() { + return config.getPlaceholderPrefix(); + } + + @Override + public String getPlaceholderSuffix() { + return config.getPlaceholderSuffix(); + } + + @Override + public String getSqlMigrationPrefix() { + return config.getSqlMigrationPrefix(); + } + + @Override + public String getRepeatableSqlMigrationPrefix() { + return config.getRepeatableSqlMigrationPrefix(); + } + + @Override + public String getSqlMigrationSeparator() { + return config.getSqlMigrationSeparator(); + } + + @Override + public String[] getSqlMigrationSuffixes() { + return config.getSqlMigrationSuffixes(); + } + + @Override + public JavaMigration[] getJavaMigrations() { + return config.getJavaMigrations(); + } + + @Override + public boolean isIgnoreMissingMigrations() { + return config.isIgnoreMissingMigrations(); + } + + @Override + public boolean isIgnoreIgnoredMigrations() { + return config.isIgnoreIgnoredMigrations(); + } + + @Override + public boolean isIgnorePendingMigrations() { + return config.isIgnorePendingMigrations(); + } + + @Override + public boolean isIgnoreFutureMigrations() { + return config.isIgnoreFutureMigrations(); + } + + @Override + public boolean isValidateMigrationNaming() { return config.isValidateMigrationNaming(); } + + @Override + public boolean isValidateOnMigrate() { + return config.isValidateOnMigrate(); + } + + @Override + public boolean isCleanOnValidationError() { + return config.isCleanOnValidationError(); + } + + @Override + public boolean isCleanDisabled() { + return config.isCleanDisabled(); + } + + @Override + public MigrationVersion getBaselineVersion() { + return config.getBaselineVersion(); + } + + @Override + public String getBaselineDescription() { + return config.getBaselineDescription(); + } + + @Override + public boolean isBaselineOnMigrate() { + return config.isBaselineOnMigrate(); + } + + @Override + public boolean isOutOfOrder() { + return config.isOutOfOrder(); + } + + @Override + public MigrationResolver[] getResolvers() { + return config.getResolvers(); + } + + @Override + public boolean isSkipDefaultResolvers() { + return config.isSkipDefaultResolvers(); + } + + @Override + public DataSource getDataSource() { + return config.getDataSource(); + } + + @Override + public int getConnectRetries() { + return config.getConnectRetries(); + } + + @Override + public String getInitSql() { + return config.getInitSql(); + } + + @Override + public ClassLoader getClassLoader() { + return config.getClassLoader(); + } + + @Override + public boolean isMixed() { + return config.isMixed(); + } + + @Override + public String getInstalledBy() { + return config.getInstalledBy(); + } + + @Override + public boolean isGroup() { + return config.isGroup(); + } + + @Override + public String[] getErrorOverrides() { + return config.getErrorOverrides(); + } + + @Override + public OutputStream getDryRunOutput() { + return config.getDryRunOutput(); + } + + @Override + public boolean isStream() { + return config.isStream(); + } + + @Override + public boolean isBatch() { + return config.isBatch(); + } + + @Override + public boolean isOracleSqlplus() { + return config.isOracleSqlplus(); + } + + @Override + public boolean isOracleSqlplusWarn() { + return config.isOracleSqlplusWarn(); + } + + @Override + public String getLicenseKey() { + return config.getLicenseKey(); + } + + @Override + public boolean outputQueryResults() { + return config.outputQueryResults(); + } + + /** + * Sets the stream where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. The stream when be closing when Flyway finishes writing the output. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutput The output file or {@code null} to execute the SQL statements directly against the database. + */ + public FluentConfiguration dryRunOutput(OutputStream dryRunOutput) { + config.setDryRunOutput(dryRunOutput); + return this; + } + + /** + * Sets the file where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. If the file specified is in a non-existent directory, Flyway will create all + * directories and parent directories as needed. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutput The output file or {@code null} to execute the SQL statements directly against the database. + */ + public FluentConfiguration dryRunOutput(File dryRunOutput) { + config.setDryRunOutputAsFile(dryRunOutput); + return this; + } + + /** + * Sets the file where to output the SQL statements of a migration dry run. {@code null} to execute the SQL statements + * directly against the database. If the file specified is in a non-existent directory, Flyway will create all + * directories and parent directories as needed. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param dryRunOutputFileName The name of the output file or {@code null} to execute the SQL statements directly + * against the database. + */ + public FluentConfiguration dryRunOutput(String dryRunOutputFileName) { + config.setDryRunOutputAsFileName(dryRunOutputFileName); + return this; + } + + /** + * Rules for the built-in error handler that let you override specific SQL states and errors codes in order to force + * specific errors or warnings to be treated as debug messages, info messages, warnings or errors. + *

Each error override has the following format: {@code STATE:12345:W}. + * It is a 5 character SQL state (or * to match all SQL states), a colon, + * the SQL error code (or * to match all SQL error codes), a colon and finally + * the desired behavior that should override the initial one.

+ *

The following behaviors are accepted:

+ *
    + *
  • {@code D} to force a debug message
  • + *
  • {@code D-} to force a debug message, but do not show the original sql state and error code
  • + *
  • {@code I} to force an info message
  • + *
  • {@code I-} to force an info message, but do not show the original sql state and error code
  • + *
  • {@code W} to force a warning
  • + *
  • {@code W-} to force a warning, but do not show the original sql state and error code
  • + *
  • {@code E} to force an error
  • + *
  • {@code E-} to force an error, but do not show the original sql state and error code
  • + *
+ *

Example 1: to force Oracle stored procedure compilation issues to produce + * errors instead of warnings, the following errorOverride can be used: {@code 99999:17110:E}

+ *

Example 2: to force SQL Server PRINT messages to be displayed as info messages (without SQL state and error + * code details) instead of warnings, the following errorOverride can be used: {@code S0001:0:I-}

+ *

Example 3: to force all errors with SQL error code 123 to be treated as warnings instead, + * the following errorOverride can be used: {@code *:123:W}

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @param errorOverrides The ErrorOverrides or an empty array if none are defined. (default: none) + */ + public FluentConfiguration errorOverrides(String... errorOverrides) { + config.setErrorOverrides(errorOverrides); + return this; + } + + /** + * Whether to group all pending migrations together in the same transaction when applying them (only recommended for databases with support for DDL transactions). + * + * @param group {@code true} if migrations should be grouped. {@code false} if they should be applied individually instead. (default: {@code false}) + */ + public FluentConfiguration group(boolean group) { + config.setGroup(group); + return this; + } + + /** + * The username that will be recorded in the schema history table as having applied the migration. + * + * @param installedBy The username or {@code null} for the current database user of the connection. (default: {@code null}). + */ + public FluentConfiguration installedBy(String installedBy) { + config.setInstalledBy(installedBy); + return this; + } + + /** + * Whether to allow mixing transactional and non-transactional statements within the same migration. Enabling this + * automatically causes the entire affected migration to be run without a transaction. + * + *

Note that this is only applicable for PostgreSQL, Aurora PostgreSQL, SQL Server and SQLite which all have + * statements that do not run at all within a transaction.

+ *

This is not to be confused with implicit transaction, as they occur in MySQL or Oracle, where even though a + * DDL statement was run within a transaction, the database will issue an implicit commit before and after + * its execution.

+ * + * @param mixed {@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false}) + */ + public FluentConfiguration mixed(boolean mixed) { + config.setMixed(mixed); + return this; + } + + /** + * Ignore missing migrations when reading the schema history table. These are migrations that were performed by an + * older deployment of the application that are no longer available in this version. For example: we have migrations + * available on the classpath with versions 1.0 and 3.0. The schema history table indicates that a migration with version 2.0 + * (unknown to us) has also been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to deploy + * a newer version of the application even though it doesn't contain migrations included with an older one anymore. + * Note that if the most recently applied migration is removed, Flyway has no way to know it is missing and will + * mark it as future instead. + * + * @param ignoreMissingMigrations {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public FluentConfiguration ignoreMissingMigrations(boolean ignoreMissingMigrations) { + config.setIgnoreMissingMigrations(ignoreMissingMigrations); + return this; + } + + /** + * Ignore ignored migrations when reading the schema history table. These are migrations that were added in between + * already migrated migrations in this version. For example: we have migrations available on the classpath with + * versions from 1.0 to 3.0. The schema history table indicates that version 1 was finished on 1.0.15, and the next + * one was 2.0.0. But with the next release a new migration was added to version 1: 1.0.16. Such scenario is ignored + * by migrate command, but by default is rejected by validate. When ignoreIgnoredMigrations is enabled, such case + * will not be reported by validate command. This is useful for situations where one must be able to deliver + * complete set of migrations in a delivery package for multiple versions of the product, and allows for further + * development of older versions. + * + * @param ignoreIgnoredMigrations {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public FluentConfiguration ignoreIgnoredMigrations(boolean ignoreIgnoredMigrations) { + config.setIgnoreIgnoredMigrations(ignoreIgnoredMigrations); + return this; + } + + /** + * Ignore pending migrations when reading the schema history table. These are migrations that are available + * but have not yet been applied. This can be useful for verifying that in-development migration changes + * don't contain any validation-breaking changes of migrations that have already been applied to a production + * environment, e.g. as part of a CI/CD process, without failing because of the existence of new migration versions. + * + * @param ignorePendingMigrations {@code true} to continue normally, {@code false} to fail fast with an exception. + * (default: {@code false}) + */ + public FluentConfiguration ignorePendingMigrations(boolean ignorePendingMigrations) { + config.setIgnorePendingMigrations(ignorePendingMigrations); + return this; + } + + /** + * Whether to ignore future migrations when reading the schema history table. These are migrations that were performed by a + * newer deployment of the application that are not yet available in this version. For example: we have migrations + * available on the classpath up to version 3.0. The schema history table indicates that a migration to version 4.0 + * (unknown to us) has already been applied. Instead of bombing out (fail fast) with an exception, a + * warning is logged and Flyway continues normally. This is useful for situations where one must be able to redeploy + * an older version of the application after the database has been migrated by a newer one. + * + * @param ignoreFutureMigrations {@code true} to continue normally and log a warning, {@code false} to fail + * fast with an exception. (default: {@code true}) + */ + public FluentConfiguration ignoreFutureMigrations(boolean ignoreFutureMigrations) { + config.setIgnoreFutureMigrations(ignoreFutureMigrations); + return this; + } + + /** + * Whether to validate migrations and callbacks whose scripts do not obey the correct naming convention. A failure can be + * useful to check that errors such as case sensitivity in migration prefixes have been corrected. + * + * @param validateMigrationNaming {@code false} to continue normally, {@code true} to fail + * fast with an exception. (default: {@code false}) + */ + public FluentConfiguration validateMigrationNaming(boolean validateMigrationNaming){ + config.setValidateMigrationNaming(validateMigrationNaming); + return this; + } + + /** + * Whether to automatically call validate or not when running migrate. + * + * @param validateOnMigrate {@code true} if validate should be called. {@code false} if not. (default: {@code true}) + */ + public FluentConfiguration validateOnMigrate(boolean validateOnMigrate) { + config.setValidateOnMigrate(validateOnMigrate); + return this; + } + + /** + * Whether to automatically call clean or not when a validation error occurs. + *

This is exclusively intended as a convenience for development. even though we + * strongly recommend not to change migration scripts once they have been checked into SCM and run, this provides a + * way of dealing with this case in a smooth manner. The database will be wiped clean automatically, ensuring that + * the next migration will bring you back to the state checked into SCM.

+ *

Warning ! Do not enable in production !

+ * + * @param cleanOnValidationError {@code true} if clean should be called. {@code false} if not. (default: {@code false}) + */ + public FluentConfiguration cleanOnValidationError(boolean cleanOnValidationError) { + config.setCleanOnValidationError(cleanOnValidationError); + return this; + } + + /** + * Whether to disable clean. + *

This is especially useful for production environments where running clean can be quite a career limiting move.

+ * + * @param cleanDisabled {@code true} to disable clean. {@code false} to leave it enabled. (default: {@code false}) + */ + public FluentConfiguration cleanDisabled(boolean cleanDisabled) { + config.setCleanDisabled(cleanDisabled); + return this; + } + + /** + * Sets the locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both SQL and Java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem, may only + * contain SQL migrations and are only scanned recursively down non-hidden directories.

+ * + * @param locations Locations to scan recursively for migrations. (default: db/migration) + */ + public FluentConfiguration locations(String... locations) { + config.setLocationsAsStrings(locations); + return this; + } + + /** + * Sets the locations to scan recursively for migrations. + *

The location type is determined by its prefix. + * Unprefixed locations or locations starting with {@code classpath:} point to a package on the classpath and may + * contain both SQL and Java-based migrations. + * Locations starting with {@code filesystem:} point to a directory on the filesystem, may only + * contain SQL migrations and are only scanned recursively down non-hidden directories.

+ * + * @param locations Locations to scan recursively for migrations. (default: db/migration) + */ + public FluentConfiguration locations(Location... locations) { + config.setLocations(locations); + return this; + } + + /** + * Sets the encoding of Sql migrations. + * + * @param encoding The encoding of Sql migrations. (default: UTF-8) + */ + public FluentConfiguration encoding(String encoding) { + config.setEncodingAsString(encoding); + return this; + } + + /** + * Sets the encoding of Sql migrations. + * + * @param encoding The encoding of Sql migrations. (default: UTF-8) + */ + public FluentConfiguration encoding(Charset encoding) { + config.setEncoding(encoding); + return this; + } + + /** + * Sets the default schema managed by Flyway. This schema name is case-sensitive. If not specified, but + * schemas is, Flyway uses the first schema in that list. If that is also not specified, Flyway uses the default + * schema for the database connection. + *

Consequences:

+ *
    + *
  • This schema will be the one containing the schema history table.
  • + *
  • This schema will be the default for the database connection (provided the database supports this concept).
  • + *
+ * + * @param schema The default schema managed by Flyway. + */ + public FluentConfiguration defaultSchema(String schema) { + config.setDefaultSchema(schema); + return this; + } + + /** + * Sets the schemas managed by Flyway. These schema names are case-sensitive. If not specified, Flyway uses + * the default schema for the database connection. If defaultSchemaName is not specified, then the first of + * this list also acts as default schema. + *

Consequences:

+ *
    + *
  • Flyway will automatically attempt to create all these schemas, unless they already exist.
  • + *
  • The schemas will be cleaned in the order of this list.
  • + *
  • If Flyway created them, the schemas themselves will be dropped when cleaning.
  • + *
+ * + * @param schemas The schemas managed by Flyway. May not be {@code null}. Must contain at least one element. + */ + public FluentConfiguration schemas(String... schemas) { + config.setSchemas(schemas); + return this; + } + + /** + *

Sets the name of the schema history table that will be used by Flyway.

By default (single-schema mode) + * the schema history table is placed in the default schema for the connection provided by the datasource.

When + * the flyway.schemas property is set (multi-schema mode), the schema history table is placed in the first schema + * of the list.

+ * + * @param table The name of the schema history table that will be used by Flyway. (default: flyway_schema_history) + */ + public FluentConfiguration table(String table) { + config.setTable(table); + return this; + } + + /** + *

Sets the tablespace where to create the schema history table that will be used by Flyway.

+ *

If not specified, Flyway uses the default tablespace for the database connection. + * This setting is only relevant for databases that do support the notion of tablespaces. Its value is simply + * ignored for all others.

+ * + * @param tablespace The tablespace where to create the schema history table that will be used by Flyway. + */ + public FluentConfiguration tablespace(String tablespace) { + config.setTablespace(tablespace); + return this; + } + + /** + * Sets the target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + */ + public FluentConfiguration target(MigrationVersion target) { + config.setTarget(target); + return this; + } + + /** + * Sets the target version up to which Flyway should consider migrations. + * Migrations with a higher version number will be ignored. + * Special values: + *
    + *
  • {@code current}: designates the current version of the schema
  • + *
  • {@code latest}: the latest version of the schema, as defined by the migration with the highest version
  • + *
+ * Defaults to {@code latest}. + */ + public FluentConfiguration target(String target) { + config.setTargetAsString(target); + return this; + } + + /** + * Sets whether placeholders should be replaced. + * + * @param placeholderReplacement Whether placeholders should be replaced. (default: true) + */ + public FluentConfiguration placeholderReplacement(boolean placeholderReplacement) { + config.setPlaceholderReplacement(placeholderReplacement); + return this; + } + + /** + * Sets the placeholders to replace in sql migration scripts. + * + * @param placeholders The map of <placeholder, replacementValue> to apply to sql migration scripts. + */ + public FluentConfiguration placeholders(Map placeholders) { + config.setPlaceholders(placeholders); + return this; + } + + /** + * Sets the prefix of every placeholder. + * + * @param placeholderPrefix The prefix of every placeholder. (default: ${ ) + */ + public FluentConfiguration placeholderPrefix(String placeholderPrefix) { + config.setPlaceholderPrefix(placeholderPrefix); + return this; + } + + /** + * Sets the suffix of every placeholder. + * + * @param placeholderSuffix The suffix of every placeholder. (default: } ) + */ + public FluentConfiguration placeholderSuffix(String placeholderSuffix) { + config.setPlaceholderSuffix(placeholderSuffix); + return this; + } + + /** + * Sets the file name prefix for sql migrations. + *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ * + * @param sqlMigrationPrefix The file name prefix for sql migrations (default: V) + */ + public FluentConfiguration sqlMigrationPrefix(String sqlMigrationPrefix) { + config.setSqlMigrationPrefix(sqlMigrationPrefix); + return this; + } + + @Override + public String getUndoSqlMigrationPrefix() { + return config.getUndoSqlMigrationPrefix(); + } + + /** + * Sets the file name prefix for undo SQL migrations. (default: U) + *

Undo SQL migrations are responsible for undoing the effects of the versioned migration with the same version.

+ *

They have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to U1.1__My_description.sql

+ *

Flyway Pro and Flyway Enterprise only

+ * + * @param undoSqlMigrationPrefix The file name prefix for undo SQL migrations. (default: U) + */ + public FluentConfiguration undoSqlMigrationPrefix(String undoSqlMigrationPrefix) { + config.setUndoSqlMigrationPrefix(undoSqlMigrationPrefix); + return this; + } + + /** + * Sets the file name prefix for repeatable sql migrations. + *

Repeatable sql migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , + * which using the defaults translates to R__My_description.sql

+ * + * @param repeatableSqlMigrationPrefix The file name prefix for repeatable sql migrations (default: R) + */ + public FluentConfiguration repeatableSqlMigrationPrefix(String repeatableSqlMigrationPrefix) { + config.setRepeatableSqlMigrationPrefix(repeatableSqlMigrationPrefix); + return this; + } + + /** + * Sets the file name separator for sql migrations. + *

Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ * + * @param sqlMigrationSeparator The file name separator for sql migrations (default: __) + */ + public FluentConfiguration sqlMigrationSeparator(String sqlMigrationSeparator) { + config.setSqlMigrationSeparator(sqlMigrationSeparator); + return this; + } + + /** + * The file name suffixes for SQL migrations. (default: .sql) + *

SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , + * which using the defaults translates to V1_1__My_description.sql

+ *

Multiple suffixes (like .sql,.pkg,.pkb) can be specified for easier compatibility with other tools such as + * editors with specific file associations.

+ * + * @param sqlMigrationSuffixes The file name suffixes for SQL migrations. + */ + public FluentConfiguration sqlMigrationSuffixes(String... sqlMigrationSuffixes) { + config.setSqlMigrationSuffixes(sqlMigrationSuffixes); + return this; + } + + /** + * The manually added Java-based migrations. These are not Java-based migrations discovered through classpath + * scanning and instantiated by Flyway. Instead these are manually added instances of JavaMigration. + * This is particularly useful when working with a dependency injection container, where you may want the DI + * container to instantiate the class and wire up its dependencies for you. + * + * @param javaMigrations The manually added Java-based migrations. An empty array if none. (default: none) + */ + public FluentConfiguration javaMigrations(JavaMigration... javaMigrations) { + config.setJavaMigrations(javaMigrations); + return this; + } + + /** + * Sets the datasource to use. Must have the necessary privileges to execute ddl. + * + * @param dataSource The datasource to use. Must have the necessary privileges to execute ddl. + */ + public FluentConfiguration dataSource(DataSource dataSource) { + config.setDataSource(dataSource); + return this; + } + + /** + * Sets the datasource to use. Must have the necessary privileges to execute ddl. + * + * @param url The JDBC URL of the database. + * @param user The user of the database. + * @param password The password of the database. + */ + public FluentConfiguration dataSource(String url, String user, String password) { + config.setDataSource(url, user, password); + return this; + } + + /** + * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will + * wait 1 second before attempting to connect again, up to the maximum number of times specified by connectRetries. + * + * @param connectRetries The maximum number of retries (default: 0). + */ + public FluentConfiguration connectRetries(int connectRetries) { + config.setConnectRetries(connectRetries); + return this; + } + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * + * @param initSql The SQL statements. (default: {@code null}) + */ + public FluentConfiguration initSql(String initSql) { + config.setInitSql(initSql); + return this; + } + + /** + * Sets the version to tag an existing schema with when executing baseline. + * + * @param baselineVersion The version to tag an existing schema with when executing baseline. (default: 1) + */ + public FluentConfiguration baselineVersion(MigrationVersion baselineVersion) { + config.setBaselineVersion(baselineVersion); + return this; + } + + /** + * Sets the version to tag an existing schema with when executing baseline. + * + * @param baselineVersion The version to tag an existing schema with when executing baseline. (default: 1) + */ + public FluentConfiguration baselineVersion(String baselineVersion) { + config.setBaselineVersion(MigrationVersion.fromVersion(baselineVersion)); + return this; + } + + /** + * Sets the description to tag an existing schema with when executing baseline. + * + * @param baselineDescription The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + public FluentConfiguration baselineDescription(String baselineDescription) { + config.setBaselineDescription(baselineDescription); + return this; + } + + /** + *

+ * Whether to automatically call baseline when migrate is executed against a non-empty schema with no schema history table. + * This schema will then be baselined with the {@code baselineVersion} before executing the migrations. + * Only migrations above {@code baselineVersion} will then be applied. + *

+ *

+ * This is useful for initial Flyway production deployments on projects with an existing DB. + *

+ *

+ * Be careful when enabling this as it removes the safety net that ensures + * Flyway does not migrate the wrong database in case of a configuration mistake! + *

+ * + * @param baselineOnMigrate {@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false}) + */ + public FluentConfiguration baselineOnMigrate(boolean baselineOnMigrate) { + config.setBaselineOnMigrate(baselineOnMigrate); + return this; + } + + /** + * Allows migrations to be run "out of order". + *

If you already have versions 1 and 3 applied, and now a version 2 is found, + * it will be applied too instead of being ignored.

+ * + * @param outOfOrder {@code true} if outOfOrder migrations should be applied, {@code false} if not. (default: {@code false}) + */ + public FluentConfiguration outOfOrder(boolean outOfOrder) { + config.setOutOfOrder(outOfOrder); + return this; + } + + /** + * Gets the callbacks for lifecycle notifications. + * + * @return The callbacks for lifecycle notifications. An empty array if none. (default: none) + */ + @Override + public Callback[] getCallbacks() { + return config.getCallbacks(); + } + + @Override + public boolean isSkipDefaultCallbacks() { + return config.isSkipDefaultCallbacks(); + } + + /** + * Set the callbacks for lifecycle notifications. + * + * @param callbacks The callbacks for lifecycle notifications. (default: none) + */ + public FluentConfiguration callbacks(Callback... callbacks) { + config.setCallbacks(callbacks); + return this; + } + + /** + * Set the callbacks for lifecycle notifications. + * + * @param callbacks The fully qualified class names of the callbacks for lifecycle notifications. (default: none) + */ + public FluentConfiguration callbacks(String... callbacks) { + config.setCallbacksAsClassNames(callbacks); + return this; + } + + /** + * Whether Flyway should skip the default callbacks. If true, only custom callbacks are used. + * + * @param skipDefaultCallbacks Whether default built-in callbacks should be skipped.

(default: false)

+ */ + public FluentConfiguration skipDefaultCallbacks(boolean skipDefaultCallbacks) { + config.setSkipDefaultCallbacks(skipDefaultCallbacks); + return this; + } + + /** + * Sets custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + * + * @param resolvers The custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. (default: empty list) + */ + public FluentConfiguration resolvers(MigrationResolver... resolvers) { + config.setResolvers(resolvers); + return this; + } + + /** + * Sets custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. + * + * @param resolvers The fully qualified class names of the custom MigrationResolvers to be used in addition to the built-in ones for resolving Migrations to apply. (default: empty list) + */ + public FluentConfiguration resolvers(String... resolvers) { + config.setResolversAsClassNames(resolvers); + return this; + } + + /** + * Whether Flyway should skip the default resolvers. If true, only custom resolvers are used. + * + * @param skipDefaultResolvers Whether default built-in resolvers should be skipped.

(default: false)

+ */ + public FluentConfiguration skipDefaultResolvers(boolean skipDefaultResolvers) { + config.setSkipDefaultResolvers(skipDefaultResolvers); + return this; + } + + /** + * Whether to stream SQL migrations when executing them. Streaming doesn't load the entire migration in memory at + * once. Instead each statement is loaded individually. This is particularly useful for very large SQL migrations + * composed of multiple MB or even GB of reference data, as this dramatically reduces Flyway's memory consumption. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param stream {@code true} to stream SQL migrations. {@code false} to fully loaded them in memory instead. (default: {@code false}) + */ + public FluentConfiguration stream(boolean stream) { + config.setStream(stream); + return this; + } + + /** + * Whether to batch SQL statements when executing them. Batching can save up to 99 percent of network roundtrips by + * sending up to 100 statements at once over the network to the database, instead of sending each statement + * individually. This is particularly useful for very large SQL migrations composed of multiple MB or even GB of + * reference data, as this can dramatically reduce the network overhead. This is supported for INSERT, UPDATE, + * DELETE, MERGE and UPSERT statements. All other statements are automatically executed without batching. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param batch {@code true} to batch SQL statements. {@code false} to execute them individually instead. (default: {@code false}) + */ + public FluentConfiguration batch(boolean batch) { + config.setBatch(batch); + return this; + } + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + *

Flyway Pro and Flyway Enterprise only

+ * + * @param oracleSqlplus {@code true} to active SQL*Plus support. {@code false} to fail fast instead. (default: {@code false}) + */ + public FluentConfiguration oracleSqlplus(boolean oracleSqlplus) { + config.setOracleSqlplus(oracleSqlplus); + return this; + } + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @param oracleSqlplusWarn {@code true} to issue a warning. {@code false} to fail fast instead. (default: {@code false}) + */ + public FluentConfiguration oracleSqlplusWarn(boolean oracleSqlplusWarn) { + config.setOracleSqlplusWarn(oracleSqlplusWarn); + return this; + } + + /** + * Your Flyway license key (FL01...). Not yet a Flyway Pro or Enterprise Edition customer? + * Request your Flyway trial license key + * to try out Flyway Pro and Enterprise Edition features free for 30 days. + * + *

Flyway Pro and Flyway Enterprise only

+ * + * @param licenseKey Your Flyway license key. + */ + public FluentConfiguration licenseKey(String licenseKey) { + config.setLicenseKey(licenseKey); + return this; + } + + /** + * Configures Flyway with these properties. This overwrites any existing configuration. Property names are + * documented in the flyway maven plugin. + *

To use a custom ClassLoader, setClassLoader() must be called prior to calling this method.

+ * + * @param properties Properties used for configuration. + * @throws FlywayException when the configuration failed. + */ + public FluentConfiguration configuration(Properties properties) { + config.configure(properties); + return this; + } + + /** + * Configures Flyway with these properties. This overwrites any existing configuration. Property names are + * documented in the flyway maven plugin. + *

To use a custom ClassLoader, it must be passed to the Flyway constructor prior to calling this method.

+ * + * @param props Properties used for configuration. + * @throws FlywayException when the configuration failed. + */ + public FluentConfiguration configuration(Map props) { + config.configure(props); + return this; + } + + /** + * Load configuration files from the default locations: + * $installationDir$/conf/flyway.conf + * $user.home$/flyway.conf + * $workingDirectory$/flyway.conf + * + * The configuration files must be encoded with UTF-8. + * + * @throws FlywayException when the configuration failed. + */ + public FluentConfiguration loadDefaultConfigurationFiles() { + return loadDefaultConfigurationFiles("UTF-8"); + } + + /** + * Load configuration files from the default locations: + * $installationDir$/conf/flyway.conf + * $user.home$/flyway.conf + * $workingDirectory$/flyway.conf + * + * @param encoding the conf file encoding. + * @throws FlywayException when the configuration failed. + */ + public FluentConfiguration loadDefaultConfigurationFiles(String encoding) { + String installationPath = ClassUtils.getLocationOnDisk(FluentConfiguration.class); + File installationDir = new File(installationPath).getParentFile(); + + Map configMap = ConfigUtils.loadDefaultConfigurationFiles(installationDir, encoding); + + config.configure(configMap); + return this; + } + + /** + * Configures Flyway using FLYWAY_* environment variables. + * + * @throws FlywayException when the configuration failed. + */ + public FluentConfiguration envVars() { + config.configureUsingEnvVars(); + return this; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/package-info.java new file mode 100644 index 00000000..e360e7d9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Flyway configuration injection. + */ +package org.flywaydb.core.api.configuration; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/executor/Context.java b/flyway-core/src/main/java/org/flywaydb/core/api/executor/Context.java new file mode 100644 index 00000000..c682487f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/executor/Context.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.executor; + +import org.flywaydb.core.api.configuration.Configuration; + +import java.sql.Connection; + +/** + * The context relevant to a migration executor. + */ +public interface Context { + /** + * @return The configuration currently in use. + */ + Configuration getConfiguration(); + + /** + * @return The JDBC connection being used. Transaction are managed by Flyway. + * When the context is passed to the migrate method, a transaction will already have + * been started if required and will be automatically committed or rolled back afterwards, unless the + * canExecuteInTransaction method has been implemented to return false. + */ + Connection getConnection(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/executor/MigrationExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/api/executor/MigrationExecutor.java new file mode 100644 index 00000000..a200c0ea --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/executor/MigrationExecutor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.executor; + +import java.sql.SQLException; + +/** + * Executes a migration. + */ +public interface MigrationExecutor { + /** + * Executes the migration this executor is associated with. + * + * @param context The context to use to execute the migration against the DB. + * @throws SQLException when the execution of a statement failed. + */ + void execute(Context context) throws SQLException; + + /** + * Whether the execution can take place inside a transaction. Almost all implementation should return {@code true}. + * This however makes it possible to execute certain migrations outside a transaction. This is useful for databases + * like PostgreSQL and SQL Server where certain statement can only execute outside a transaction. + * + * @return {@code true} if a transaction should be used (highly recommended), or {@code false} if not. + */ + boolean canExecuteInTransaction(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/executor/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/executor/package-info.java new file mode 100644 index 00000000..d25f38f7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/executor/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Migration executors. + */ +package org.flywaydb.core.api.executor; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/logging/Log.java b/flyway-core/src/main/java/org/flywaydb/core/api/logging/Log.java new file mode 100644 index 00000000..83326562 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/logging/Log.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.logging; + +/** + * A logger. + */ +public interface Log { + /** + * @return Whether debug logging is enabled. + */ + boolean isDebugEnabled(); + + /** + * Logs a debug message. + * + * @param message The message to log. + */ + void debug(String message); + + /** + * Logs an info message. + * + * @param message The message to log. + */ + void info(String message); + + /** + * Logs a warning message. + * + * @param message The message to log. + */ + void warn(String message); + + /** + * Logs an error message. + * + * @param message The message to log. + */ + void error(String message); + + /** + * Logs an error message and the exception that caused it. + * + * @param message The message to log. + * @param e The exception that caused the error. + */ + void error(String message, Exception e); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogCreator.java b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogCreator.java new file mode 100644 index 00000000..8e04e2ae --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogCreator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.logging; + +/** + * Factory for implementation-specific loggers. + */ +public interface LogCreator { + /** + * Creates an implementation-specific logger for this class. + * + * @param clazz The class to create the logger for. + * @return The logger. + */ + Log createLogger(Class clazz); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java new file mode 100644 index 00000000..2671d431 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.logging; + +import org.flywaydb.core.internal.logging.LogCreatorFactory; + +/** + * Factory for loggers. Custom MigrationResolver, MigrationExecutor, Callback and JavaMigration + * implementations should use this to obtain a logger that will work with any logging framework across all environments + * (API, Maven, Gradle, CLI, etc). + */ +public class LogFactory { + /** + * Factory for implementation-specific loggers. + */ + private static LogCreator logCreator; + + /** + * The factory for implementation-specific loggers to be used as a fallback when no other suitable loggers were found. + */ + private static LogCreator fallbackLogCreator; + + /** + * Prevent instantiation. + */ + private LogFactory() { + // Do nothing + } + + /** + * Sets the LogCreator that will be used. This will effectively override Flyway's default LogCreator auto-detection + * logic and force Flyway to always use this LogCreator regardless of which log libraries are present on the + * classpath. + * + *

This is primarily meant for integrating Flyway into environments with their own logging system (like Ant, + * Gradle, Maven, ...). This ensures Flyway is a good citizen in those environments and sends its logs through the + * expected pipeline.

+ * + * @param logCreator The factory for implementation-specific loggers. + */ + public static void setLogCreator(LogCreator logCreator) { + LogFactory.logCreator = logCreator; + } + + /** + * Sets the fallback LogCreator. This LogCreator will be used as a fallback solution when the default LogCreator + * auto-detection logic fails to detect a suitable LogCreator based on the log libraries present on the classpath. + * + * @param fallbackLogCreator The factory for implementation-specific loggers to be used as a fallback when no other + * suitable loggers were found. + */ + public static void setFallbackLogCreator(LogCreator fallbackLogCreator) { + LogFactory.fallbackLogCreator = fallbackLogCreator; + } + + /** + * Retrieves the matching logger for this class. + * + * @param clazz The class to get the logger for. + * @return The logger. + */ + public static Log getLog(Class clazz) { + if (logCreator == null) { + logCreator = LogCreatorFactory.getLogCreator(LogFactory.class.getClassLoader(), fallbackLogCreator); + } + + return logCreator.createLogger(clazz); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/logging/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/logging/package-info.java new file mode 100644 index 00000000..1555fa80 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/logging/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Flyway's log abstraction. Custom MigrationResolver, MigrationExecutor, FlywayCallback, ErrorHandler and JdbcMigration + * implementations should use this to obtain a logger that will work with any logging framework across all environments + * (API, Maven, Gradle, CLI, etc). + */ +package org.flywaydb.core.api.logging; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/migration/BaseJavaMigration.java b/flyway-core/src/main/java/org/flywaydb/core/api/migration/BaseJavaMigration.java new file mode 100644 index 00000000..9eb31a13 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/migration/BaseJavaMigration.java @@ -0,0 +1,110 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.migration; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.internal.resolver.MigrationInfoHelper; +import org.flywaydb.core.internal.util.Pair; + +/** + *

This is the recommended class to extend for implementing Java-based Migrations.

+ *

Subclasses should follow the default Flyway naming convention of having a class name with the following structure:

+ *
    + *
  • Versioned Migrations: V2__Add_new_table
  • + *
  • Undo Migrations: U2__Add_new_table
  • + *
  • Repeatable Migrations: R__Add_new_table
  • + *
+ * + *

The file name consists of the following parts:

+ *
    + *
  • Prefix: V for versioned migrations, U for undo migrations, R for repeatable migrations
  • + *
  • Version: Underscores (automatically replaced by dots at runtime) separate as many parts as you like (Not for repeatable migrations)
  • + *
  • Separator: __ (two underscores)
  • + *
  • Description: Underscores (automatically replaced by spaces at runtime) separate the words
  • + *
+ *

If you need more control over the class name, you can override the default convention by implementing the + * JavaMigration interface directly. This will allow you to name your class as you wish. Version, description and + * migration category are provided by implementing the respective methods.

+ */ +public abstract class BaseJavaMigration implements JavaMigration { + private final MigrationVersion version; + private final String description; + + + + + /** + * Creates a new instance of a Java-based migration following Flyway's default naming convention. + */ + public BaseJavaMigration() { + String shortName = getClass().getSimpleName(); + String prefix; + + + + boolean repeatable = shortName.startsWith("R"); + if (shortName.startsWith("V") || repeatable + + + + ) { + prefix = shortName.substring(0, 1); + } else { + throw new FlywayException("Invalid Java-based migration class name: " + getClass().getName() + + " => ensure it starts with V" + + + + + " or R," + + " or implement org.flywaydb.core.api.migration.JavaMigration directly for non-default naming"); + } + Pair info = + MigrationInfoHelper.extractVersionAndDescription(shortName, prefix, "__", new String[]{""}, repeatable); + version = info.getLeft(); + description = info.getRight(); + } + + @Override + public MigrationVersion getVersion() { + return version; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public Integer getChecksum() { + return null; + } + + @Override + public boolean isUndo() { + + + + + return false; + + } + + @Override + public boolean canExecuteInTransaction() { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/migration/Context.java b/flyway-core/src/main/java/org/flywaydb/core/api/migration/Context.java new file mode 100644 index 00000000..6dd9666f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/migration/Context.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.migration; + +import org.flywaydb.core.api.configuration.Configuration; + +import java.sql.Connection; + +/** + * The context relevant to a Java-based migration. + */ +public interface Context { + /** + * @return The configuration currently in use. + */ + Configuration getConfiguration(); + + /** + * @return The JDBC connection being used. Transaction are managed by Flyway. + * When the context is passed to the migrate method, a transaction will already have + * been started if required and will be automatically committed or rolled back afterwards, unless the + * canExecuteInTransaction method has been implemented to return false. + */ + Connection getConnection(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/migration/JavaMigration.java b/flyway-core/src/main/java/org/flywaydb/core/api/migration/JavaMigration.java new file mode 100644 index 00000000..d2c699cf --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/migration/JavaMigration.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.migration; + +import org.flywaydb.core.api.MigrationVersion; + +/** + * Interface to be implemented by Java-based Migrations. + * + *

Java-based migrations are a great fit for all changes that can not easily be expressed using SQL.

+ * + *

These would typically be things like

+ *
    + *
  • BLOB & CLOB changes
  • + *
  • Advanced bulk data changes (Recalculations, advanced format changes, …)
  • + *
+ * + *

Migration classes implementing this interface will be + * automatically discovered when placed in a location on the classpath.

+ * + *

Most users will be better served by subclassing subclass {@link BaseJavaMigration} instead of implementing this + * interface directly, as {@link BaseJavaMigration} encourages the use of Flyway's default naming convention and + * comes with sensible default implementations of all methods (except migrate of course) while at the same time also + * providing better isolation against future additions to this interface.

+ */ +public interface JavaMigration { + /** + * @return The version of the schema after the migration is complete. {@code null} for repeatable migrations. + */ + MigrationVersion getVersion(); + + /** + * @return The description of this migration for the migration history. Never {@code null}. + */ + String getDescription(); + + /** + * Computes the checksum of the migration. + * + * @return The checksum of the migration. + */ + Integer getChecksum(); + + /** + * Whether this is an undo migration for a previously applied versioned migration. + * + * @return {@code true} if it is, {@code false} if not. Always {@code false} for repeatable migrations. + */ + boolean isUndo(); + + /** + * Whether the execution should take place inside a transaction. Almost all implementation should return {@code true}. + * This however makes it possible to execute certain migrations outside a transaction. This is useful for databases + * like PostgreSQL and SQL Server where certain statement can only execute outside a transaction. + * + * @return {@code true} if a transaction should be used (highly recommended), or {@code false} if not. + */ + boolean canExecuteInTransaction(); + + /** + * Executes this migration. The execution will automatically take place within a transaction, when the underlying + * database supports it and the canExecuteInTransaction returns {@code true}. + * + * @param context The context relevant for this migration, containing things like the JDBC connection to use and the + * current Flyway configuration. + * @throws Exception when the migration failed. + */ + void migrate(Context context) throws Exception; +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/migration/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/migration/package-info.java new file mode 100644 index 00000000..d6d590ee --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/migration/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Migration implementors. + */ +package org.flywaydb.core.api.migration; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/package-info.java new file mode 100644 index 00000000..f7ea4c0a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * FlywayException, MigrationInfo and related classes. + */ +package org.flywaydb.core.api; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ChecksumMatcher.java b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ChecksumMatcher.java new file mode 100644 index 00000000..2986649a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ChecksumMatcher.java @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.resolver; + +interface ChecksumMatcher { + boolean checksumMatches(Integer checksum); + + boolean checksumMatchesWithoutBeingIdentical(Integer checksum); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/Context.java b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/Context.java new file mode 100644 index 00000000..62b46df1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/Context.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.resolver; + +import org.flywaydb.core.api.configuration.Configuration; + +import java.sql.Connection; + +/** + * The context relevant to a migration resolver. + */ +public interface Context { + /** + * @return The configuration currently in use. + */ + Configuration getConfiguration(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/MigrationResolver.java b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/MigrationResolver.java new file mode 100644 index 00000000..1c6b9bfb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/MigrationResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.resolver; + +import java.util.Collection; + +/** + * Resolves available migrations. This interface can be implemented to create custom resolvers. A custom resolver + * can be used to create additional types of migrations not covered by the standard resolvers (jdbc, sql, spring-jdbc). + * Using the skipDefaultResolvers configuration property, the built-in resolvers can also be completely replaced. + */ +public interface MigrationResolver { + /** + * Resolves the available migrations. + * + * @return The available migrations. + */ + Collection resolveMigrations(Context context); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ResolvedMigration.java b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ResolvedMigration.java new file mode 100644 index 00000000..b5bf08fd --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ResolvedMigration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.api.resolver; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.executor.MigrationExecutor; + +/** + * Migration resolved through a MigrationResolver. Can be applied against a database. + */ +public interface ResolvedMigration extends ChecksumMatcher { + /** + * @return The version of the database after applying this migration. {@code null} for repeatable migrations. + */ + MigrationVersion getVersion(); + + /** + * @return The description of the migration. + */ + String getDescription(); + + /** + * @return The name of the script to execute for this migration, relative to its base (classpath/filesystem) location. + */ + String getScript(); + + /** + * @return The checksum of the migration. Optional. Can be {@code null} if not unique checksum is computable. + */ + Integer getChecksum(); + + /** + * @return The type of migration (INIT, SQL, ...) + */ + MigrationType getType(); + + /** + * @return The physical location of the migration on disk. Used for more precise error reporting in case of conflict. + */ + String getPhysicalLocation(); + + /** + * @return The executor to run this migration. + */ + MigrationExecutor getExecutor(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/package-info.java new file mode 100644 index 00000000..13fd55ac --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Interfaces for Migration resolvers. + */ +package org.flywaydb.core.api.resolver; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/CallbackExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/CallbackExecutor.java new file mode 100644 index 00000000..1bf6b07f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/CallbackExecutor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.callback.Warning; +import org.flywaydb.core.api.callback.Error; + +import java.util.List; + +/** + * Executes the callbacks for a specific event. + */ +public interface CallbackExecutor { + /** + * Executes the callbacks for this event on the main connection, within a separate transaction per callback if possible. + * + * @param event The vent to handle. + */ + void onEvent(Event event); + + /** + * Executes the callbacks for this event on the migration connection, within a separate transaction per callback if possible. + * + * @param event The vent to handle. + */ + void onMigrateOrUndoEvent(Event event); + + /** + * Sets the current migration info. + * + * @param migrationInfo The current migration. + */ + void setMigrationInfo(MigrationInfo migrationInfo); + + /** + * Executes the callbacks for an "each" event within the same transaction (if any) as the main operation. + * + * @param event The event to handle. + */ + void onEachMigrateOrUndoEvent(Event event); + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/DefaultCallbackExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/DefaultCallbackExecutor.java new file mode 100644 index 00000000..00849fbf --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/DefaultCallbackExecutor.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.callback.Warning; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Executes the callbacks for a specific event. + */ +public class DefaultCallbackExecutor implements CallbackExecutor { + private final Configuration configuration; + private final Database database; + private final Schema schema; + private final Collection callbacks; + private MigrationInfo migrationInfo; + + /** + * Creates a new callback executor. + * + * @param configuration The configuration. + * @param database The database. + * @param schema The current schema to use for the connection. + * @param callbacks The callbacks to execute. + */ + public DefaultCallbackExecutor(Configuration configuration, Database database, Schema schema, Collection callbacks) { + this.configuration = configuration; + this.database = database; + this.schema = schema; + this.callbacks = callbacks; + } + + @Override + public void onEvent(final Event event) { + execute(event, database.getMainConnection()); + } + + @Override + public void onMigrateOrUndoEvent(final Event event) { + execute(event, database.getMigrationConnection()); + } + + @Override + public void setMigrationInfo(MigrationInfo migrationInfo) { + this.migrationInfo = migrationInfo; + } + + @Override + public void onEachMigrateOrUndoEvent(Event event) { + final Context context = new SimpleContext(configuration, database.getMigrationConnection(), migrationInfo); + for (Callback callback : callbacks) { + if (callback.supports(event, context)) { + callback.handle(event, context); + } + } + } + + + + + + + + + + + + + + + private void execute(final Event event, final Connection connection) { + final Context context = new SimpleContext(configuration, connection, null); + + for (final Callback callback : callbacks) { + if (callback.supports(event, context)) { + if (callback.canHandleInTransaction(event, context)) { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + DefaultCallbackExecutor.this.execute(connection, callback, event, context); + return null; + } + }); + } else { + execute(connection, callback, event, context); + } + } + } + } + + private void execute(Connection connection, Callback callback, Event event, Context context) { + connection.restoreOriginalState(); + connection.changeCurrentSchemaTo(schema); + handleEvent(callback, event, context); + } + + private void handleEvent(Callback callback, Event event, Context context) { + try { + callback.handle(event, context); + } catch (RuntimeException e) { + throw new FlywayException("Error while executing " + event.getId() + " callback: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallback.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallback.java new file mode 100644 index 00000000..99ac6a4c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallback.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; + +/** + * Callback that does nothing. + */ +public enum NoopCallback implements Callback { + INSTANCE; + + @Override + public boolean supports(Event event, Context context) { + return false; + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } + + @Override + public void handle(Event event, Context context) { + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallbackExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallbackExecutor.java new file mode 100644 index 00000000..90b2f2d1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallbackExecutor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.callback.Warning; + +import java.util.List; + +/** + * A callback executor that does nothing. + */ +public enum NoopCallbackExecutor implements CallbackExecutor { + INSTANCE; + + @Override + public void onEvent(Event event) { + } + + @Override + public void onMigrateOrUndoEvent(Event event) { + } + + @Override + public void setMigrationInfo(MigrationInfo migrationInfo) { + } + + @Override + public void onEachMigrateOrUndoEvent(Event event) { + } + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SimpleContext.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SimpleContext.java new file mode 100644 index 00000000..004e4bda --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SimpleContext.java @@ -0,0 +1,96 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Statement; +import org.flywaydb.core.api.callback.Warning; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Connection; + +import java.util.List; + +public class SimpleContext implements Context { + private final Configuration configuration; + private final Connection connection; + private final MigrationInfo migrationInfo; + private final Statement statement; + + SimpleContext(Configuration configuration, Connection connection, MigrationInfo migrationInfo) { + this.configuration = configuration; + this.connection = connection; + this.migrationInfo = migrationInfo; + this.statement = null; + } + + public SimpleContext(Configuration configuration, Connection connection, MigrationInfo migrationInfo, + String sql, List warnings, List errors) { + this.configuration = configuration; + this.connection = connection; + this.migrationInfo = migrationInfo; + this.statement = new SimpleStatement(sql, warnings, errors); + } + + @Override + public Configuration getConfiguration() { + return configuration; + } + + @Override + public java.sql.Connection getConnection() { + return connection.getJdbcConnection(); + } + + @Override + public MigrationInfo getMigrationInfo() { + return migrationInfo; + } + + @Override + public Statement getStatement() { + return statement; + } + + + private static class SimpleStatement implements Statement { + private final String sql; + private final List warnings; + private final List errors; + + private SimpleStatement(String sql, List warnings, List errors) { + this.sql = sql; + this.warnings = warnings; + this.errors = errors; + } + + @Override + public String getSql() { + return sql; + } + + @Override + public List getWarnings() { + return warnings; + } + + @Override + public List getErrors() { + return errors; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SqlScriptCallbackFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SqlScriptCallbackFactory.java new file mode 100644 index 00000000..018d82b6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SqlScriptCallbackFactory.java @@ -0,0 +1,157 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.callback; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceName; +import org.flywaydb.core.internal.resource.ResourceNameParser; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Callback factory, looking for SQL scripts (named like on the callback methods) inside the configured locations. + */ +public class SqlScriptCallbackFactory { + private static final Log LOG = LogFactory.getLog(SqlScriptCallbackFactory.class); + + private final List callbacks = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param resourceProvider The resource provider. + * @param sqlScriptFactory The SQL statement factory. + * @param configuration The Flyway configuration. + */ + public SqlScriptCallbackFactory(ResourceProvider resourceProvider, + SqlScriptExecutorFactory sqlScriptExecutorFactory, + SqlScriptFactory sqlScriptFactory, + Configuration configuration) { + Map callbacksFound = new HashMap<>(); + + LOG.debug("Scanning for SQL callbacks ..."); + Collection resources = resourceProvider.getResources("", configuration.getSqlMigrationSuffixes()); + ResourceNameParser resourceNameParser = new ResourceNameParser(configuration); + + for (LoadableResource resource : resources) { + ResourceName parsedName = resourceNameParser.parse(resource.getFilename()); + if (!parsedName.isValid()) { + continue; + } + + String name = parsedName.getFilenameWithoutSuffix(); + Event event = Event.fromId(parsedName.getPrefix()); + if (event != null) { + SqlScript existing = callbacksFound.get(name); + if (existing != null) { + throw new FlywayException("Found more than 1 SQL callback script called " + name + "!\n" + + "Offenders:\n" + + "-> " + existing.getResource().getAbsolutePathOnDisk() + "\n" + + "-> " + resource.getAbsolutePathOnDisk()); + } + SqlScript sqlScript = sqlScriptFactory.createSqlScript(resource, configuration.isMixed(), resourceProvider); + callbacksFound.put(name, sqlScript); + callbacks.add(new SqlScriptCallback(event, parsedName.getDescription(), sqlScriptExecutorFactory, sqlScript + + + + )); + } + } + Collections.sort(callbacks); + } + + public List getCallbacks() { + return new ArrayList<>(callbacks); + } + + private static class SqlScriptCallback implements Callback, Comparable { + private final Event event; + private final String description; + private final SqlScriptExecutorFactory sqlScriptExecutorFactory; + private final SqlScript sqlScript; + + + + + private SqlScriptCallback(Event event, String description, SqlScriptExecutorFactory sqlScriptExecutorFactory, SqlScript sqlScript + + + + ) { + this.event = event; + this.description = description; + this.sqlScriptExecutorFactory = sqlScriptExecutorFactory; + this.sqlScript = sqlScript; + + + + } + + @Override + public boolean supports(Event event, Context context) { + return this.event == event; + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return sqlScript.executeInTransaction(); + } + + @Override + public void handle(Event event, Context context) { + LOG.info("Executing SQL callback: " + event.getId() + + (description == null ? "" : " - " + description) + + (sqlScript.executeInTransaction() ? "" : " [non-transactional]")); + sqlScriptExecutorFactory.createSqlScriptExecutor(context.getConnection() + + + + ).execute(sqlScript); + } + + @Override + public int compareTo(SqlScriptCallback o) { + int result = event.compareTo(o.event); + if (result == 0) { + if (description == null) { + return -1; + } + if (o.description == null) { + return 1; + } + result = description.compareTo(o.description); + } + return result; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/package-info.java new file mode 100644 index 00000000..b189d1e8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * FlywayException, MigrationInfo and related classes. + */ +package org.flywaydb.core.internal.callback; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/ClassProvider.java b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/ClassProvider.java new file mode 100644 index 00000000..a6078c0e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/ClassProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.clazz; + +import java.util.Collection; + +/** + * A facility to obtain classes. + */ +public interface ClassProvider { + /** + * Retrieve all classes which implement the specified interface. + * + * @return The non-abstract classes that were found. + */ + Collection> getClasses(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.java b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.java new file mode 100644 index 00000000..fa7f710e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.clazz; + +import java.util.Collection; +import java.util.Collections; + +/** + * ClassProvider that does nothing. + */ +public enum NoopClassProvider implements ClassProvider { + INSTANCE; + + @Override + public Collection> getClasses() { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/package-info.java new file mode 100644 index 00000000..85d24975 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.clazz; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbBaseline.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbBaseline.java new file mode 100644 index 00000000..8993977b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbBaseline.java @@ -0,0 +1,117 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.schemahistory.AppliedMigration; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; + +/** + * Handles Flyway's baseline command. + */ +public class DbBaseline { + private static final Log LOG = LogFactory.getLog(DbBaseline.class); + + /** + * The schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The version to tag an existing schema with when executing baseline. + */ + private final MigrationVersion baselineVersion; + + /** + * The description to tag an existing schema with when executing baseline. + */ + private final String baselineDescription; + + /** + * The callback executor. + */ + private final CallbackExecutor callbackExecutor; + + /** + * Creates a new DbBaseline. + * + * @param schemaHistory The database schema history table. + * @param baselineVersion The version to tag an existing schema with when executing baseline. + * @param baselineDescription The description to tag an existing schema with when executing baseline. + * @param callbackExecutor The callback executor. + */ + public DbBaseline(SchemaHistory schemaHistory, MigrationVersion baselineVersion, + String baselineDescription, CallbackExecutor callbackExecutor) { + this.schemaHistory = schemaHistory; + this.baselineVersion = baselineVersion; + this.baselineDescription = baselineDescription; + this.callbackExecutor = callbackExecutor; + } + + /** + * Baselines the database. + */ + public void baseline() { + callbackExecutor.onEvent(Event.BEFORE_BASELINE); + + try { + if (!schemaHistory.exists()) { + schemaHistory.create(true); + LOG.info("Successfully baselined schema with version: " + baselineVersion); + } else { + AppliedMigration baselineMarker = schemaHistory.getBaselineMarker(); + if (baselineMarker != null) { + if (baselineVersion.equals(baselineMarker.getVersion()) + && baselineDescription.equals(baselineMarker.getDescription())) { + LOG.info("Schema history table " + schemaHistory + " already initialized with (" + + baselineVersion + "," + baselineDescription + "). Skipping."); + } else { + throw new FlywayException("Unable to baseline schema history table " + schemaHistory + " with (" + + baselineVersion + "," + baselineDescription + + ") as it has already been baselined with (" + + baselineMarker.getVersion() + "," + baselineMarker.getDescription() + ")"); + } + } else { + if (schemaHistory.hasSchemasMarker() && baselineVersion.equals(MigrationVersion.fromVersion("0"))) { + throw new FlywayException("Unable to baseline schema history table " + schemaHistory + " with version 0 as this version was used for schema creation"); + } + + if (schemaHistory.hasNonSyntheticAppliedMigrations()) { + throw new FlywayException("Unable to baseline schema history table " + schemaHistory + " as it already contains migrations"); + } + + if (schemaHistory.allAppliedMigrations().isEmpty()) { + throw new FlywayException("Unable to baseline schema history table " + schemaHistory + " as it already exists, and is empty.\n" + + "Delete the schema history table with the clean command, and run baseline again."); + } + + throw new FlywayException("Unable to baseline schema history table " + schemaHistory + " as it already contains migrations.\n" + + "Delete the schema history table with the clean command, and run baseline again."); + } + } + } catch (FlywayException e) { + callbackExecutor.onEvent(Event.AFTER_BASELINE_ERROR); + throw e; + } + + callbackExecutor.onEvent(Event.AFTER_BASELINE); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbClean.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbClean.java new file mode 100644 index 00000000..4e39fb7f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbClean.java @@ -0,0 +1,248 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.StopWatch; +import org.flywaydb.core.internal.util.TimeFormat; + +import java.util.concurrent.Callable; + +/** + * Main workflow for cleaning the database. + */ +public class DbClean { + private static final Log LOG = LogFactory.getLog(DbClean.class); + + /** + * The connection to use. + */ + private final Connection connection; + + /** + * The schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The schemas to clean. + */ + private final Schema[] schemas; + + /** + * The callback executor. + */ + private final CallbackExecutor callbackExecutor; + + /** + * Whether to disable clean. + *

This is especially useful for production environments where running clean can be quite a career limiting move.

+ */ + private boolean cleanDisabled; + + /** + * The database + */ + private Database database; + + /** + * Creates a new database cleaner. + * + * @param database The DB support for the connection. + * @param schemaHistory The schema history table. + * @param schemas The schemas to clean. + * @param callbackExecutor The callback executor. + * @param cleanDisabled Whether to disable clean. + */ + public DbClean(Database database, SchemaHistory schemaHistory, Schema[] schemas, + CallbackExecutor callbackExecutor, boolean cleanDisabled) { + this.database = database; + this.connection = database.getMainConnection(); + this.schemaHistory = schemaHistory; + this.schemas = schemas; + this.callbackExecutor = callbackExecutor; + this.cleanDisabled = cleanDisabled; + } + + /** + * Cleans the schemas of all objects. + * + * @throws FlywayException when clean failed. + */ + public void clean() throws FlywayException { + if (cleanDisabled) { + throw new FlywayException("Unable to execute clean as it has been disabled with the \"flyway.cleanDisabled\" property."); + } + callbackExecutor.onEvent(Event.BEFORE_CLEAN); + + try { + connection.changeCurrentSchemaTo(schemas[0]); + boolean dropSchemas = false; + try { + dropSchemas = schemaHistory.hasSchemasMarker(); + } catch (Exception e) { + LOG.error("Error while checking whether the schemas should be dropped", e); + } + + dropDatabaseObjectsPreSchemas(); + + for (Schema schema : schemas) { + if (!schema.exists()) { + LOG.warn("Unable to clean unknown schema: " + schema); + continue; + } + + if (dropSchemas) { + dropSchema(schema); + } else { + cleanSchema(schema); + } + } + + dropDatabaseObjectsPostSchemas(); + + } catch (FlywayException e) { + callbackExecutor.onEvent(Event.AFTER_CLEAN_ERROR); + throw e; + } + + callbackExecutor.onEvent(Event.AFTER_CLEAN); + schemaHistory.clearCache(); + } + + /** + * Drops database-level objects that need to be cleaned prior to schema-level objects + * + * @throws FlywayException when the drop failed. + */ + private void dropDatabaseObjectsPreSchemas() { + LOG.debug("Dropping pre-schema database level objects..."); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + try { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + database.cleanPreSchemas(); + return null; + } + }); + } catch (FlywaySqlException e) { + LOG.debug(e.getMessage()); + LOG.warn("Unable to drop pre-schema database level objects"); + } + stopWatch.stop(); + LOG.info(String.format("Successfully dropped pre-schema database level objects (execution time %s)", + TimeFormat.format(stopWatch.getTotalTimeMillis()))); + } + + /** + * Drops database-level objects that need to be cleaned after all schema-level objects + * + * @throws FlywayException when the drop failed. + */ + private void dropDatabaseObjectsPostSchemas() { + LOG.debug("Dropping post-schema database level objects..."); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + try { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + database.cleanPostSchemas(); + return null; + } + }); + } catch (FlywaySqlException e) { + LOG.debug(e.getMessage()); + LOG.warn("Unable to drop post-schema database level objects"); + } + stopWatch.stop(); + LOG.info(String.format("Successfully dropped post-schema database level objects (execution time %s)", + TimeFormat.format(stopWatch.getTotalTimeMillis()))); + } + + /** + * Drops this schema. + * + * @param schema The schema to drop. + * @throws FlywayException when the drop failed. + */ + private void dropSchema(final Schema schema) { + LOG.debug("Dropping schema " + schema + " ..."); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + try { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + schema.drop(); + return null; + } + }); + } catch (FlywaySqlException e) { + LOG.debug(e.getMessage()); + LOG.warn("Unable to drop schema " + schema + ". Attempting clean instead..."); + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + schema.clean(); + return null; + } + }); + } + stopWatch.stop(); + LOG.info(String.format("Successfully dropped schema %s (execution time %s)", + schema, TimeFormat.format(stopWatch.getTotalTimeMillis()))); + } + + /** + * Cleans this schema of all objects. + * + * @param schema The schema to clean. + * @throws FlywayException when clean failed. + */ + private void cleanSchema(final Schema schema) { + LOG.debug("Cleaning schema " + schema + " ..."); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Void call() { + schema.clean(); + return null; + } + }); + stopWatch.stop(); + LOG.info(String.format("Successfully cleaned schema %s (execution time %s)", + schema, TimeFormat.format(stopWatch.getTotalTimeMillis()))); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbInfo.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbInfo.java new file mode 100644 index 00000000..a2afdc1c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbInfo.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.info.MigrationInfoServiceImpl; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; + +public class DbInfo { + private final MigrationResolver migrationResolver; + private final SchemaHistory schemaHistory; + private final Configuration configuration; + private final CallbackExecutor callbackExecutor; + + public DbInfo(MigrationResolver migrationResolver, SchemaHistory schemaHistory, + Configuration configuration, CallbackExecutor callbackExecutor) { + + this.migrationResolver = migrationResolver; + this.schemaHistory = schemaHistory; + this.configuration = configuration; + this.callbackExecutor = callbackExecutor; + } + + public MigrationInfoService info() { + callbackExecutor.onEvent(Event.BEFORE_INFO); + + + MigrationInfoServiceImpl migrationInfoService; + try { + migrationInfoService = + new MigrationInfoServiceImpl(migrationResolver, schemaHistory, configuration, + configuration.getTarget(), configuration.isOutOfOrder(), + true, true, true, true); + migrationInfoService.refresh(); + } catch (FlywayException e) { + callbackExecutor.onEvent(Event.AFTER_INFO_ERROR); + throw e; + } + + callbackExecutor.onEvent(Event.AFTER_INFO); + + return migrationInfoService; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbMigrate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbMigrate.java new file mode 100644 index 00000000..3a6f9f62 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbMigrate.java @@ -0,0 +1,428 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationState; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.executor.Context; +import org.flywaydb.core.api.executor.MigrationExecutor; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.info.MigrationInfoImpl; +import org.flywaydb.core.internal.info.MigrationInfoServiceImpl; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.ExceptionUtils; +import org.flywaydb.core.internal.util.StopWatch; +import org.flywaydb.core.internal.util.StringUtils; +import org.flywaydb.core.internal.util.TimeFormat; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * Main workflow for migrating the database. + */ +public class DbMigrate { + private static final Log LOG = LogFactory.getLog(DbMigrate.class); + + /** + * Database-specific functionality. + */ + private final Database database; + + /** + * The database schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The schema containing the schema history table. + */ + private final Schema schema; + + /** + * The migration resolver. + */ + private final MigrationResolver migrationResolver; + + /** + * The Flyway configuration. + */ + private final Configuration configuration; + + /** + * The callback executor. + */ + private final CallbackExecutor callbackExecutor; + + /** + * The connection to use to perform the actual database migrations. + */ + private final Connection connectionUserObjects; + + /** + * Creates a new database migrator. + * + * @param database Database-specific functionality. + * @param schemaHistory The database schema history table. + * @param migrationResolver The migration resolver. + * @param configuration The Flyway configuration. + * @param callbackExecutor The callbacks executor. + */ + public DbMigrate(Database database, + SchemaHistory schemaHistory, Schema schema, MigrationResolver migrationResolver, + Configuration configuration, CallbackExecutor callbackExecutor) { + this.database = database; + this.connectionUserObjects = database.getMigrationConnection(); + this.schemaHistory = schemaHistory; + this.schema = schema; + this.migrationResolver = migrationResolver; + this.configuration = configuration; + this.callbackExecutor = callbackExecutor; + } + + /** + * Starts the actual migration. + * + * @return The number of successfully applied migrations. + * @throws FlywayException when migration failed. + */ + public int migrate() throws FlywayException { + callbackExecutor.onMigrateOrUndoEvent(Event.BEFORE_MIGRATE); + + int count; + try { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + count = configuration.isGroup() ? + // When group is active, start the transaction boundary early to + // ensure that all changes to the schema history table are either committed or rolled back atomically. + schemaHistory.lock(new Callable() { + @Override + public Integer call() { + return migrateAll(); + } + }) : + // For all regular cases, proceed with the migration as usual. + migrateAll(); + + stopWatch.stop(); + + logSummary(count, stopWatch.getTotalTimeMillis()); + } catch (FlywayException e) { + callbackExecutor.onMigrateOrUndoEvent(Event.AFTER_MIGRATE_ERROR); + throw e; + } + + callbackExecutor.onMigrateOrUndoEvent(Event.AFTER_MIGRATE); + return count; + } + + private int migrateAll() { + int total = 0; + while (true) { + final boolean firstRun = total == 0; + int count = configuration.isGroup() + // With group active a lock on the schema history table has already been acquired. + ? migrateGroup(firstRun) + // Otherwise acquire the lock now. The lock will be released at the end of each migration. + : schemaHistory.lock(new Callable() { + @Override + public Integer call() { + return migrateGroup(firstRun); + } + }); + total += count; + if (count == 0) { + // No further migrations available + break; + } + } + return total; + } + + /** + * Migrate a group of one (group = false) or more (group = true) migrations. + * + * @param firstRun Where this is the first time this code runs in this migration run. + * @return The number of newly applied migrations. + */ + private Integer migrateGroup(boolean firstRun) { + MigrationInfoServiceImpl infoService = + new MigrationInfoServiceImpl(migrationResolver, schemaHistory, configuration, + configuration.getTarget(), configuration.isOutOfOrder(), + true, true, true, true); + infoService.refresh(); + + MigrationInfo current = infoService.current(); + MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion(); + if (firstRun) { + LOG.info("Current version of schema " + schema + ": " + currentSchemaVersion); + + if (configuration.isOutOfOrder()) { + LOG.warn("outOfOrder mode is active. Migration of schema " + schema + " may not be reproducible."); + } + } + + MigrationInfo[] future = infoService.future(); + if (future.length > 0) { + List resolved = Arrays.asList(infoService.resolved()); + Collections.reverse(resolved); + if (resolved.isEmpty()) { + LOG.warn("Schema " + schema + " has version " + currentSchemaVersion + + ", but no migration could be resolved in the configured locations ! Note this warning will become an error in Flyway 7."); + } else { + for (MigrationInfo migrationInfo : resolved) { + // Only consider versioned migrations + if (migrationInfo.getVersion() != null) { + LOG.warn("Schema " + schema + " has a version (" + currentSchemaVersion + + ") that is newer than the latest available migration (" + + migrationInfo.getVersion() + ") !"); + break; + } + } + } + } + + MigrationInfo[] failed = infoService.failed(); + if (failed.length > 0) { + if ((failed.length == 1) + && (failed[0].getState() == MigrationState.FUTURE_FAILED) + && configuration.isIgnoreFutureMigrations()) { + LOG.warn("Schema " + schema + " contains a failed future migration to version " + failed[0].getVersion() + " !"); + } else { + if (failed[0].getVersion() == null) { + throw new FlywayException("Schema " + schema + " contains a failed repeatable migration (" + failed[0].getDescription() + ") !"); + } + throw new FlywayException("Schema " + schema + " contains a failed migration to version " + failed[0].getVersion() + " !"); + } + } + + LinkedHashMap group = new LinkedHashMap<>(); + for (MigrationInfoImpl pendingMigration : infoService.pending()) { + boolean isOutOfOrder = pendingMigration.getVersion() != null + && pendingMigration.getVersion().compareTo(currentSchemaVersion) < 0; + group.put(pendingMigration, isOutOfOrder); + + if (!configuration.isGroup()) { + // Only include one pending migration if group is disabled + break; + } + } + + if (!group.isEmpty()) { + applyMigrations(group); + } + return group.size(); + } + + /** + * Logs the summary of this migration run. + * + * @param migrationSuccessCount The number of successfully applied migrations. + * @param executionTime The total time taken to perform this migration run (in ms). + */ + + private void logSummary(int migrationSuccessCount, long executionTime) { + if (migrationSuccessCount == 0) { + LOG.info("Schema " + schema + " is up to date. No migration necessary."); + return; + } + + if (migrationSuccessCount == 1) { + LOG.info("Successfully applied 1 migration to schema " + schema + " (execution time " + TimeFormat.format(executionTime) + ")"); + } else { + LOG.info("Successfully applied " + migrationSuccessCount + " migrations to schema " + schema + " (execution time " + TimeFormat.format(executionTime) + ")"); + } + } + + /** + * Applies this migration to the database. The migration state and the execution time are updated accordingly. + * + * @param group The group of migrations to apply. + */ + private void applyMigrations(final LinkedHashMap group) { + boolean executeGroupInTransaction = isExecuteGroupInTransaction(group); + final StopWatch stopWatch = new StopWatch(); + try { + if (executeGroupInTransaction) { + ExecutionTemplateFactory.createExecutionTemplate(connectionUserObjects.getJdbcConnection(), database).execute(new Callable() { + @Override + public Object call() { + doMigrateGroup(group, stopWatch); + return null; + } + }); + } else { + doMigrateGroup(group, stopWatch); + } + } catch (FlywayMigrateException e) { + MigrationInfoImpl migration = e.getMigration(); + String failedMsg = "Migration of " + toMigrationText(migration, e.isOutOfOrder()) + " failed!"; + if (database.supportsDdlTransactions() && executeGroupInTransaction) { + LOG.error(failedMsg + " Changes successfully rolled back."); + } else { + LOG.error(failedMsg + " Please restore backups and roll back database and code!"); + + stopWatch.stop(); + int executionTime = (int) stopWatch.getTotalTimeMillis(); + schemaHistory.addAppliedMigration(migration.getVersion(), migration.getDescription(), + migration.getType(), migration.getScript(), migration.getResolvedMigration().getChecksum(), executionTime, false); + } + throw e; + } + } + + private boolean isExecuteGroupInTransaction(LinkedHashMap group) { + boolean executeGroupInTransaction = true; + boolean first = true; + + for (Map.Entry entry : group.entrySet()) { + ResolvedMigration resolvedMigration = entry.getKey().getResolvedMigration(); + boolean inTransaction = resolvedMigration.getExecutor().canExecuteInTransaction(); + + if (first) { + executeGroupInTransaction = inTransaction; + first = false; + continue; + } + + if (!configuration.isMixed() && executeGroupInTransaction != inTransaction) { + throw new FlywayException( + "Detected both transactional and non-transactional migrations within the same migration group" + + " (even though mixed is false). First offending migration:" + + (resolvedMigration.getVersion() == null ? "" : " " + resolvedMigration.getVersion()) + + (StringUtils.hasLength(resolvedMigration.getDescription()) ? " " + resolvedMigration.getDescription() : "") + + (inTransaction ? "" : " [non-transactional]")); + } + + executeGroupInTransaction &= inTransaction; + } + + return executeGroupInTransaction; + } + + private void doMigrateGroup(LinkedHashMap group, StopWatch stopWatch) { + Context context = new Context() { + @Override + public Configuration getConfiguration() { + return configuration; + } + + @Override + public java.sql.Connection getConnection() { + return connectionUserObjects.getJdbcConnection(); + } + }; + + for (Map.Entry entry : group.entrySet()) { + final MigrationInfoImpl migration = entry.getKey(); + boolean isOutOfOrder = entry.getValue(); + + final String migrationText = toMigrationText(migration, isOutOfOrder); + + stopWatch.start(); + + LOG.debug("Starting migration of " + migrationText + " ..."); + + connectionUserObjects.restoreOriginalState(); + connectionUserObjects.changeCurrentSchemaTo(schema); + + try { + callbackExecutor.setMigrationInfo(migration); + callbackExecutor.onEachMigrateOrUndoEvent(Event.BEFORE_EACH_MIGRATE); + try { + LOG.info("Migrating " + migrationText); + migration.getResolvedMigration().getExecutor().execute(context); + } catch (FlywayException e) { + callbackExecutor.onEachMigrateOrUndoEvent(Event.AFTER_EACH_MIGRATE_ERROR); + throw new FlywayMigrateException(migration, isOutOfOrder, e); + } catch (SQLException e) { + callbackExecutor.onEachMigrateOrUndoEvent(Event.AFTER_EACH_MIGRATE_ERROR); + throw new FlywayMigrateException(migration, isOutOfOrder, e); + } + + LOG.debug("Successfully completed migration of " + migrationText); + callbackExecutor.onEachMigrateOrUndoEvent(Event.AFTER_EACH_MIGRATE); + } finally { + callbackExecutor.setMigrationInfo(null); + } + + stopWatch.stop(); + int executionTime = (int) stopWatch.getTotalTimeMillis(); + + schemaHistory.addAppliedMigration(migration.getVersion(), migration.getDescription(), migration.getType(), + migration.getScript(), migration.getResolvedMigration().getChecksum(), executionTime, true); + } + } + + private String toMigrationText(MigrationInfoImpl migration, boolean isOutOfOrder) { + final MigrationExecutor migrationExecutor = migration.getResolvedMigration().getExecutor(); + final String migrationText; + if (migration.getVersion() != null) { + migrationText = "schema " + schema + " to version " + migration.getVersion() + + (StringUtils.hasLength(migration.getDescription()) ? " - " + migration.getDescription() : "") + + (isOutOfOrder ? " [out of order]" : "") + + (migrationExecutor.canExecuteInTransaction() ? "" : " [non-transactional]"); + } else { + migrationText = "schema " + schema + " with repeatable migration " + migration.getDescription() + + (migrationExecutor.canExecuteInTransaction() ? "" : " [non-transactional]"); + } + return migrationText; + } + + public static class FlywayMigrateException extends FlywayException { + private final MigrationInfoImpl migration; + private final boolean outOfOrder; + + FlywayMigrateException(MigrationInfoImpl migration, boolean outOfOrder, SQLException e) { + super(ExceptionUtils.toMessage(e), e); + this.migration = migration; + this.outOfOrder = outOfOrder; + } + + FlywayMigrateException(MigrationInfoImpl migration, boolean outOfOrder, FlywayException e) { + super(e.getMessage(), e); + this.migration = migration; + this.outOfOrder = outOfOrder; + } + + public MigrationInfoImpl getMigration() { + return migration; + } + + public boolean isOutOfOrder() { + return outOfOrder; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbRepair.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbRepair.java new file mode 100644 index 00000000..5ae649d3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbRepair.java @@ -0,0 +1,177 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationState; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.info.MigrationInfoImpl; +import org.flywaydb.core.internal.info.MigrationInfoServiceImpl; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.schemahistory.AppliedMigration; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.StopWatch; +import org.flywaydb.core.internal.util.TimeFormat; + +import java.util.Objects; +import java.util.concurrent.Callable; + +/** + * Handles Flyway's repair command. + */ +public class DbRepair { + private static final Log LOG = LogFactory.getLog(DbRepair.class); + + /** + * The database connection to use for accessing the schema history table. + */ + private final Connection connection; + + /** + * The migration infos. + */ + private final MigrationInfoServiceImpl migrationInfoService; + + /** + * The schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The callback executor. + */ + private final CallbackExecutor callbackExecutor; + + /** + * The database-specific support. + */ + private final Database database; + + /** + * Creates a new DbRepair. + * + * @param database The database-specific support. + * @param migrationResolver The migration resolver. + * @param schemaHistory The schema history table. + * @param callbackExecutor The callback executor. + */ + public DbRepair(Database database, MigrationResolver migrationResolver, SchemaHistory schemaHistory, + CallbackExecutor callbackExecutor, Configuration configuration) { + this.database = database; + this.connection = database.getMainConnection(); + this.migrationInfoService = new MigrationInfoServiceImpl(migrationResolver, schemaHistory, configuration, + MigrationVersion.LATEST, true, true, true, true, true); + this.schemaHistory = schemaHistory; + this.callbackExecutor = callbackExecutor; + } + + /** + * Repairs the schema history table. + */ + public void repair() { + callbackExecutor.onEvent(Event.BEFORE_REPAIR); + + try { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + boolean repaired = ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), database).execute(new Callable() { + public Boolean call() { + schemaHistory.removeFailedMigrations(); + migrationInfoService.refresh(); + + return alignAppliedMigrationsWithResolvedMigrations(); + } + }); + + stopWatch.stop(); + + LOG.info("Successfully repaired schema history table " + schemaHistory + " (execution time " + + TimeFormat.format(stopWatch.getTotalTimeMillis()) + ")."); + if (repaired && !database.supportsDdlTransactions()) { + LOG.info("Manual cleanup of the remaining effects the failed migration may still be required."); + } + } catch (FlywayException e) { + callbackExecutor.onEvent(Event.AFTER_REPAIR_ERROR); + throw e; + } + + callbackExecutor.onEvent(Event.AFTER_REPAIR); + } + + private boolean alignAppliedMigrationsWithResolvedMigrations() { + boolean repaired = false; + for (MigrationInfo migrationInfo : migrationInfoService.all()) { + MigrationInfoImpl migrationInfoImpl = (MigrationInfoImpl) migrationInfo; + + ResolvedMigration resolved = migrationInfoImpl.getResolvedMigration(); + AppliedMigration applied = migrationInfoImpl.getAppliedMigration(); + if (resolved != null + && resolved.getVersion() != null + && applied != null + && !applied.getType().isSynthetic() + + + + && updateNeeded(resolved, applied)) { + schemaHistory.update(applied, resolved); + repaired = true; + } + + if (resolved != null + && resolved.getVersion() == null + && applied != null + && !applied.getType().isSynthetic() + + + + && resolved.checksumMatchesWithoutBeingIdentical(applied.getChecksum())) { + schemaHistory.update(applied, resolved); + repaired = true; + } + } + + return repaired; + } + + private boolean updateNeeded(ResolvedMigration resolved, AppliedMigration applied) { + return checksumUpdateNeeded(resolved, applied) + || descriptionUpdateNeeded(resolved, applied) + || typeUpdateNeeded(resolved, applied); + } + + private boolean checksumUpdateNeeded(ResolvedMigration resolved, AppliedMigration applied) { + return !resolved.checksumMatches(applied.getChecksum()); + } + + private boolean descriptionUpdateNeeded(ResolvedMigration resolved, AppliedMigration applied) { + return !Objects.equals(resolved.getDescription(), applied.getDescription()); + } + + private boolean typeUpdateNeeded(ResolvedMigration resolved, AppliedMigration applied) { + return !Objects.equals(resolved.getType(), applied.getType()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbSchemas.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbSchemas.java new file mode 100644 index 00000000..395fd5a5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbSchemas.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Handles Flyway's automatic schema creation. + */ +public class DbSchemas { + private static final Log LOG = LogFactory.getLog(DbSchemas.class); + + /** + * The database connection to use for accessing the schema history table. + */ + private final Connection connection; + + /** + * The schemas managed by Flyway. + */ + private final Schema[] schemas; + + /** + * The schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The database + */ + private final Database database; + + /** + * Creates a new DbSchemas. + * + * @param database The database to use. + * @param schemas The schemas managed by Flyway. + * @param schemaHistory The schema history table. + */ + public DbSchemas(Database database, Schema[] schemas, SchemaHistory schemaHistory) { + this.database = database; + this.connection = database.getMainConnection(); + this.schemas = schemas; + this.schemaHistory = schemaHistory; + } + + /** + * Creates the schemas. + * + * @param baseline Whether to include the creation of a baseline marker. + */ + public void create(final boolean baseline) { + int retries = 0; + while (true) { + try { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), database).execute(new Callable() { + @Override + public Void call() { + List createdSchemas = new ArrayList<>(); + for (Schema schema : schemas) { + if (!schema.exists()) { + if (schema.getName() == null) { + throw new FlywayException("Unable to determine schema for the schema history table." + + " Set a default schema for the connection or specify one using the defaultSchema property!"); + } + LOG.debug("Creating schema: " + schema); + schema.create(); + createdSchemas.add(schema); + } else { + LOG.debug("Skipping creation of existing schema: " + schema); + } + } + + if (!createdSchemas.isEmpty()) { + schemaHistory.create(baseline); + schemaHistory.addSchemasMarker(createdSchemas.toArray(new Schema[0])); + } + + return null; + } + }); + return; + } catch (RuntimeException e) { + if (++retries >= 10) { + throw e; + } + try { + LOG.debug("Schema creation failed. Retrying in 1 sec ..."); + Thread.sleep(1000); + } catch (InterruptedException e1) { + // Ignore + } + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbValidate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbValidate.java new file mode 100644 index 00000000..fab43fb2 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbValidate.java @@ -0,0 +1,174 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.command; + +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.info.MigrationInfoServiceImpl; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.Pair; +import org.flywaydb.core.internal.util.StopWatch; +import org.flywaydb.core.internal.util.TimeFormat; + +import java.util.concurrent.Callable; + +/** + * Handles the validate command. + * + * @author Axel Fontaine + */ +public class DbValidate { + private static final Log LOG = LogFactory.getLog(DbValidate.class); + + /** + * The database schema history table. + */ + private final SchemaHistory schemaHistory; + + /** + * The schema containing the schema history table. + */ + private final Schema schema; + + /** + * The migration resolver. + */ + private final MigrationResolver migrationResolver; + + /** + * The connection to use. + */ + private final Connection connection; + + /** + * The current configuration. + */ + private final Configuration configuration; + + /** + * Whether pending migrations are allowed. + */ + private final boolean pending; + + /** + * The callback executor. + */ + private final CallbackExecutor callbackExecutor; + + private final Database database; + + /** + * Creates a new database validator. + * + * @param database The DB support for the connection. + * @param schemaHistory The database schema history table. + * @param schema The database schema to use by default. + * @param migrationResolver The migration resolver. + * @param configuration The current configuration. + * @param pending Whether pending migrations are allowed. + * @param callbackExecutor The callback executor. + */ + public DbValidate(Database database, SchemaHistory schemaHistory, Schema schema, MigrationResolver migrationResolver, + Configuration configuration, boolean pending, CallbackExecutor callbackExecutor) { + this.database = database; + this.connection = database.getMainConnection(); + this.schemaHistory = schemaHistory; + this.schema = schema; + this.migrationResolver = migrationResolver; + this.configuration = configuration; + this.pending = pending; + this.callbackExecutor = callbackExecutor; + } + + /** + * Starts the actual migration. + * + * @return The validation error, if any. + */ + public String validate() { + if (!schema.exists()) { + if (!migrationResolver.resolveMigrations(new Context() { + @Override + public Configuration getConfiguration() { + return configuration; + } + }).isEmpty() && !pending) { + return "Schema " + schema + " doesn't exist yet"; + } + return null; + } + + callbackExecutor.onEvent(Event.BEFORE_VALIDATE); + + LOG.debug("Validating migrations ..."); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + Pair result = ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable>() { + @Override + public Pair call() { + MigrationInfoServiceImpl migrationInfoService = + new MigrationInfoServiceImpl(migrationResolver, schemaHistory, configuration, + configuration.getTarget(), + configuration.isOutOfOrder(), + pending, + configuration.isIgnoreMissingMigrations(), + configuration.isIgnoreIgnoredMigrations(), + configuration.isIgnoreFutureMigrations()); + + migrationInfoService.refresh(); + + int count = migrationInfoService.all().length; + String validationError = migrationInfoService.validate(); + return Pair.of(count, validationError); + } + }); + + stopWatch.stop(); + + String error = result.getRight(); + if (error == null) { + int count = result.getLeft(); + if (count == 1) { + LOG.info(String.format("Successfully validated 1 migration (execution time %s)", + TimeFormat.format(stopWatch.getTotalTimeMillis()))); + } else { + LOG.info(String.format("Successfully validated %d migrations (execution time %s)", + count, TimeFormat.format(stopWatch.getTotalTimeMillis()))); + + if (count == 0) { + LOG.warn("No migrations found. Are your locations set up correctly?"); + } + } + callbackExecutor.onEvent(Event.AFTER_VALIDATE); + } else { + callbackExecutor.onEvent(Event.AFTER_VALIDATE_ERROR); + } + + + return error; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/command/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/command/package-info.java new file mode 100644 index 00000000..b3f88bb5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/command/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.command; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.java new file mode 100644 index 00000000..c1c45ad0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.java @@ -0,0 +1,551 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.configuration; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.FileCopyUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Configuration-related utilities. + */ +public class ConfigUtils { + private static Log LOG = LogFactory.getLog(ConfigUtils.class); + + /** + * The default configuration file name. + */ + public static final String CONFIG_FILE_NAME = "flyway.conf"; + public static final String CONFIG_FILES = "flyway.configFiles"; + public static final String CONFIG_FILE_ENCODING = "flyway.configFileEncoding"; + public static final String BASELINE_DESCRIPTION = "flyway.baselineDescription"; + public static final String BASELINE_ON_MIGRATE = "flyway.baselineOnMigrate"; + public static final String BASELINE_VERSION = "flyway.baselineVersion"; + public static final String BATCH = "flyway.batch"; + public static final String CALLBACKS = "flyway.callbacks"; + public static final String CLEAN_DISABLED = "flyway.cleanDisabled"; + public static final String CLEAN_ON_VALIDATION_ERROR = "flyway.cleanOnValidationError"; + public static final String CONNECT_RETRIES = "flyway.connectRetries"; + public static final String DEFAULT_SCHEMA = "flyway.defaultSchema"; + public static final String DRIVER = "flyway.driver"; + public static final String DRYRUN_OUTPUT = "flyway.dryRunOutput"; + public static final String ENCODING = "flyway.encoding"; + public static final String ERROR_OVERRIDES = "flyway.errorOverrides"; + public static final String GROUP = "flyway.group"; + public static final String IGNORE_FUTURE_MIGRATIONS = "flyway.ignoreFutureMigrations"; + public static final String IGNORE_MISSING_MIGRATIONS = "flyway.ignoreMissingMigrations"; + public static final String IGNORE_IGNORED_MIGRATIONS = "flyway.ignoreIgnoredMigrations"; + public static final String IGNORE_PENDING_MIGRATIONS = "flyway.ignorePendingMigrations"; + public static final String INIT_SQL = "flyway.initSql"; + public static final String INSTALLED_BY = "flyway.installedBy"; + public static final String LICENSE_KEY = "flyway.licenseKey"; + public static final String LOCATIONS = "flyway.locations"; + public static final String MIXED = "flyway.mixed"; + public static final String OUT_OF_ORDER = "flyway.outOfOrder"; + public static final String OUTPUT_QUERY_RESULTS = "flyway.outputQueryResults"; + public static final String PASSWORD = "flyway.password"; + public static final String PLACEHOLDER_PREFIX = "flyway.placeholderPrefix"; + public static final String PLACEHOLDER_REPLACEMENT = "flyway.placeholderReplacement"; + public static final String PLACEHOLDER_SUFFIX = "flyway.placeholderSuffix"; + public static final String PLACEHOLDERS_PROPERTY_PREFIX = "flyway.placeholders."; + public static final String REPEATABLE_SQL_MIGRATION_PREFIX = "flyway.repeatableSqlMigrationPrefix"; + public static final String RESOLVERS = "flyway.resolvers"; + public static final String SCHEMAS = "flyway.schemas"; + public static final String SKIP_DEFAULT_CALLBACKS = "flyway.skipDefaultCallbacks"; + public static final String SKIP_DEFAULT_RESOLVERS = "flyway.skipDefaultResolvers"; + public static final String SQL_MIGRATION_PREFIX = "flyway.sqlMigrationPrefix"; + public static final String SQL_MIGRATION_SEPARATOR = "flyway.sqlMigrationSeparator"; + public static final String SQL_MIGRATION_SUFFIXES = "flyway.sqlMigrationSuffixes"; + public static final String STREAM = "flyway.stream"; + public static final String TABLE = "flyway.table"; + public static final String TABLESPACE = "flyway.tablespace"; + public static final String TARGET = "flyway.target"; + public static final String UNDO_SQL_MIGRATION_PREFIX = "flyway.undoSqlMigrationPrefix"; + public static final String URL = "flyway.url"; + public static final String USER = "flyway.user"; + public static final String VALIDATE_ON_MIGRATE = "flyway.validateOnMigrate"; + public static final String VALIDATE_MIGRATION_NAMING = "flyway.validateMigrationNaming"; + + // Oracle-specific + public static final String ORACLE_SQLPLUS = "flyway.oracle.sqlplus"; + public static final String ORACLE_SQLPLUS_WARN = "flyway.oracle.sqlplusWarn"; + + // Command-line specific + public static final String JAR_DIRS = "flyway.jarDirs"; + + // Gradle specific + public static final String CONFIGURATIONS = "flyway.configurations"; + + private ConfigUtils() { + // Utility class + } + + /** + * Converts Flyway-specific environment variables to their matching properties. + * + * @return The properties corresponding to the environment variables. + */ + public static Map environmentVariablesToPropertyMap() { + Map result = new HashMap<>(); + + for (Map.Entry entry : System.getenv().entrySet()) { + String convertedKey = convertKey(entry.getKey()); + if (convertedKey != null) { + // Known environment variable + result.put(convertKey(entry.getKey()), entry.getValue()); + } + } + + return result; + } + + private static String convertKey(String key) { + if ("FLYWAY_BASELINE_DESCRIPTION".equals(key)) { + return BASELINE_DESCRIPTION; + } + if ("FLYWAY_BASELINE_ON_MIGRATE".equals(key)) { + return BASELINE_ON_MIGRATE; + } + if ("FLYWAY_BASELINE_VERSION".equals(key)) { + return BASELINE_VERSION; + } + if ("FLYWAY_BATCH".equals(key)) { + return BATCH; + } + if ("FLYWAY_CALLBACKS".equals(key)) { + return CALLBACKS; + } + if ("FLYWAY_CLEAN_DISABLED".equals(key)) { + return CLEAN_DISABLED; + } + if ("FLYWAY_CLEAN_ON_VALIDATION_ERROR".equals(key)) { + return CLEAN_ON_VALIDATION_ERROR; + } + if ("FLYWAY_CONFIG_FILE_ENCODING".equals(key)) { + return CONFIG_FILE_ENCODING; + } + if ("FLYWAY_CONFIG_FILES".equals(key)) { + return CONFIG_FILES; + } + if ("FLYWAY_CONNECT_RETRIES".equals(key)) { + return CONNECT_RETRIES; + } + if ("FLYWAY_DEFAULT_SCHEMA".equals(key)) { + return DEFAULT_SCHEMA; + } + if ("FLYWAY_DRIVER".equals(key)) { + return DRIVER; + } + if ("FLYWAY_DRYRUN_OUTPUT".equals(key)) { + return DRYRUN_OUTPUT; + } + if ("FLYWAY_ENCODING".equals(key)) { + return ENCODING; + } + if ("FLYWAY_ERROR_OVERRIDES".equals(key)) { + return ERROR_OVERRIDES; + } + if ("FLYWAY_GROUP".equals(key)) { + return GROUP; + } + if ("FLYWAY_IGNORE_FUTURE_MIGRATIONS".equals(key)) { + return IGNORE_FUTURE_MIGRATIONS; + } + if ("FLYWAY_IGNORE_MISSING_MIGRATIONS".equals(key)) { + return IGNORE_MISSING_MIGRATIONS; + } + if ("FLYWAY_IGNORE_IGNORED_MIGRATIONS".equals(key)) { + return IGNORE_IGNORED_MIGRATIONS; + } + if ("FLYWAY_IGNORE_PENDING_MIGRATIONS".equals(key)) { + return IGNORE_PENDING_MIGRATIONS; + } + if ("FLYWAY_INIT_SQL".equals(key)) { + return INIT_SQL; + } + if ("FLYWAY_INSTALLED_BY".equals(key)) { + return INSTALLED_BY; + } + if ("FLYWAY_LICENSE_KEY".equals(key)) { + return LICENSE_KEY; + } + if ("FLYWAY_LOCATIONS".equals(key)) { + return LOCATIONS; + } + if ("FLYWAY_MIXED".equals(key)) { + return MIXED; + } + if ("FLYWAY_OUT_OF_ORDER".equals(key)) { + return OUT_OF_ORDER; + } + if ("FLYWAY_OUTPUT_QUERY_RESULTS".equals(key)) { + return OUTPUT_QUERY_RESULTS; + } + if ("FLYWAY_PASSWORD".equals(key)) { + return PASSWORD; + } + if ("FLYWAY_PLACEHOLDER_PREFIX".equals(key)) { + return PLACEHOLDER_PREFIX; + } + if ("FLYWAY_PLACEHOLDER_REPLACEMENT".equals(key)) { + return PLACEHOLDER_REPLACEMENT; + } + if ("FLYWAY_PLACEHOLDER_SUFFIX".equals(key)) { + return PLACEHOLDER_SUFFIX; + } + if (key.matches("FLYWAY_PLACEHOLDERS_.+")) { + return PLACEHOLDERS_PROPERTY_PREFIX + key.substring("FLYWAY_PLACEHOLDERS_".length()).toLowerCase(Locale.ENGLISH); + } + if ("FLYWAY_REPEATABLE_SQL_MIGRATION_PREFIX".equals(key)) { + return REPEATABLE_SQL_MIGRATION_PREFIX; + } + if ("FLYWAY_RESOLVERS".equals(key)) { + return RESOLVERS; + } + if ("FLYWAY_SCHEMAS".equals(key)) { + return SCHEMAS; + } + if ("FLYWAY_SKIP_DEFAULT_CALLBACKS".equals(key)) { + return SKIP_DEFAULT_CALLBACKS; + } + if ("FLYWAY_SKIP_DEFAULT_RESOLVERS".equals(key)) { + return SKIP_DEFAULT_RESOLVERS; + } + if ("FLYWAY_SQL_MIGRATION_PREFIX".equals(key)) { + return SQL_MIGRATION_PREFIX; + } + if ("FLYWAY_SQL_MIGRATION_SEPARATOR".equals(key)) { + return SQL_MIGRATION_SEPARATOR; + } + if ("FLYWAY_SQL_MIGRATION_SUFFIXES".equals(key)) { + return SQL_MIGRATION_SUFFIXES; + } + if ("FLYWAY_STREAM".equals(key)) { + return STREAM; + } + if ("FLYWAY_TABLE".equals(key)) { + return TABLE; + } + if ("FLYWAY_TABLESPACE".equals(key)) { + return TABLESPACE; + } + if ("FLYWAY_TARGET".equals(key)) { + return TARGET; + } + if ("FLYWAY_UNDO_SQL_MIGRATION_PREFIX".equals(key)) { + return UNDO_SQL_MIGRATION_PREFIX; + } + if ("FLYWAY_URL".equals(key)) { + return URL; + } + if ("FLYWAY_USER".equals(key)) { + return USER; + } + if ("FLYWAY_VALIDATE_ON_MIGRATE".equals(key)) { + return VALIDATE_ON_MIGRATE; + } + + // Oracle-specific + if ("FLYWAY_ORACLE_SQLPLUS".equals(key)) { + return ORACLE_SQLPLUS; + } + if ("FLYWAY_ORACLE_SQLPLUS_WARN".equals(key)) { + return ORACLE_SQLPLUS_WARN; + } + + // Command-line specific + if ("FLYWAY_JAR_DIRS".equals(key)) { + return JAR_DIRS; + } + + // Gradle specific + if ("FLYWAY_CONFIGURATIONS".equals(key)) { + return CONFIGURATIONS; + } + + return null; + } + + /** + * Load configuration files from the default locations: + * $installationDir$/conf/flyway.conf + * $user.home$/flyway.conf + * $workingDirectory$/flyway.conf + * + * @param encoding the conf file encoding. + * @throws FlywayException when the configuration failed. + */ + public static Map loadDefaultConfigurationFiles(File installationDir, String encoding) { + Map configMap = new HashMap<>(); + configMap.putAll(ConfigUtils.loadConfigurationFile(new File(installationDir.getAbsolutePath() + "/conf/" + ConfigUtils.CONFIG_FILE_NAME), encoding, false)); + configMap.putAll(ConfigUtils.loadConfigurationFile(new File(System.getProperty("user.home") + "/" + ConfigUtils.CONFIG_FILE_NAME), encoding, false)); + configMap.putAll(ConfigUtils.loadConfigurationFile(new File(ConfigUtils.CONFIG_FILE_NAME), encoding, false)); + + return configMap; + } + + /** + * Loads the configuration from this configuration file. + * + * @param configFile The configuration file to load. + * @param encoding The encoding of the configuration file. + * @param failIfMissing Whether to fail if the file is missing. + * @return The properties from the configuration file. An empty Map if none. + * @throws FlywayException when the configuration file could not be loaded. + */ + public static Map loadConfigurationFile(File configFile, String encoding, boolean failIfMissing) throws FlywayException { + String errorMessage = "Unable to load config file: " + configFile.getAbsolutePath(); + + if (!configFile.isFile() || !configFile.canRead()) { + if (!failIfMissing) { + LOG.debug(errorMessage); + return new HashMap<>(); + } + throw new FlywayException(errorMessage); + } + + LOG.debug("Loading config file: " + configFile.getAbsolutePath()); + + try { + return loadConfigurationFromReader(new InputStreamReader(new FileInputStream(configFile), encoding)); + } catch (IOException | FlywayException e) { + throw new FlywayException(errorMessage, e); + } + } + + /** + * Reads the configuration from a Reader. + * + * @param reader The reader used to read the configuration. + * @return The properties from the configuration file. An empty Map if none. + * @throws FlywayException when the configuration could not be read. + */ + public static Map loadConfigurationFromReader(Reader reader) throws FlywayException { + try { + String contents = FileCopyUtils.copyToString(reader); + return loadConfigurationFromString(contents); + } catch (IOException e) { + throw new FlywayException("Unable to read config", e); + } + } + + public static Map loadConfigurationFromString(String configuration) throws IOException { + String[] lines = configuration.split("\n"); + + StringBuilder confBuilder = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + String replacedLine = line.trim().replace("\\", "\\\\"); + + // if the line ends in a \\, then it may be a multiline property + if (replacedLine.endsWith("\\\\")) { + + // if we arent the last line + if (i < lines.length-1) { + // look ahead to see if the next line is a property, a blank line, or another multiline + String nextLine = lines[i+1]; + + boolean restoreMultilineDelimiter = false; + if (nextLine.isEmpty()) { + // blank line + } else if (nextLine.contains("=")) { + // property + } else { + // line with content, this was a multiline property + restoreMultilineDelimiter = true; + } + + if (restoreMultilineDelimiter) { + // its a multiline property, so restore the original single slash + replacedLine = replacedLine.substring(0, replacedLine.length()-2) + "\\"; + } + } + } + + confBuilder.append(replacedLine).append("\n"); + } + String contents = confBuilder.toString(); + + Properties properties = new Properties(); + contents = expandEnvironmentVariables(contents, System.getenv()); + properties.load(new StringReader(contents)); + return propertiesToMap(properties); + } + + + static String expandEnvironmentVariables(String value, Map environmentVariables) { + Pattern pattern = Pattern.compile("\\$\\{([A-Za-z0-9_]+)}"); + Matcher matcher = pattern.matcher(value); + String expandedValue = value; + + while (matcher.find()) { + String variableName = matcher.group(1); + String variableValue = environmentVariables.containsKey(variableName) + ? environmentVariables.get(variableName) + : ""; + + LOG.debug("Expanding environment variable in config: " + variableName + " -> " + variableValue); + expandedValue = expandedValue.replaceAll(Pattern.quote(matcher.group(0)), Matcher.quoteReplacement(variableValue)); + } + + return expandedValue; + } + + /** + * Converts this Properties object into a map. + * + * @param properties The Properties object to convert. + * @return The resulting map. + */ + public static Map propertiesToMap(Properties properties) { + Map props = new HashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + props.put(entry.getKey().toString(), entry.getValue().toString()); + } + return props; + } + + /** + * Puts this property in the config if it has been set in any of these values. + * + * @param config The config. + * @param key The property name. + * @param values The values to try. The first non-null value will be set. + */ + public static void putIfSet(Map config, String key, Object... values) { + for (Object value : values) { + if (value != null) { + config.put(key, value.toString()); + return; + } + } + } + + /** + * Puts this property in the config if it has been set in any of these values. + * + * @param config The config. + * @param key The property name. + * @param values The values to try. The first non-null value will be set. + */ + public static void putArrayIfSet(Map config, String key, String[]... values) { + for (String[] value : values) { + if (value != null) { + config.put(key, StringUtils.arrayToCommaDelimitedString(value)); + return; + } + } + } + + /** + * Removes this property from the config. + * + * @param config The config. + * @param key The property name. + * @return The property value as a boolean if it exists, otherwise null. + * @throws FlywayException when the property value is not a valid boolean. + */ + public static Boolean removeBoolean(Map config, String key) { + String value = config.remove(key); + if (value == null) { + return null; + } + if (!"true".equals(value) && !"false".equals(value)) { + throw new FlywayException("Invalid value for " + key + " (should be either true or false): " + value, + ErrorCode.CONFIGURATION); + } + return Boolean.valueOf(value); + } + + /** + * Removes this property from the config. + * + * @param config The config. + * @param key The property name. + * @return The property value as an integer if it exists, otherwise null. + * @throws FlywayException when the property value is not a valid integer. + */ + public static Integer removeInteger(Map config, String key) { + String value = config.remove(key); + if (value == null) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new FlywayException("Invalid value for " + key + " (should be an integer): " + value, + ErrorCode.CONFIGURATION); + } + } + + /** + * Dumps the configuration to the console when debug output is activated. + * + * @param config The configured properties. + */ + public static void dumpConfiguration(Map config) { + if (LOG.isDebugEnabled()) { + LOG.debug("Using configuration:"); + for (Map.Entry entry : new TreeMap<>(config).entrySet()) { + String value = entry.getValue(); + + switch (entry.getKey()) { + // Mask the password. Ex.: T0pS3cr3t -> ********* + case ConfigUtils.PASSWORD: + value = StringUtils.trimOrPad("", value.length(), '*'); + break; + // Mask the licence key, leaving a few characters to confirm which key is in use + case ConfigUtils.LICENSE_KEY: + value = value.substring(0, 8) + "******" + value.substring(value.length() - 4); + break; + } + + LOG.debug(entry.getKey() + " -> " + value); + } + } + } + + /** + * Checks the configuration for any unrecognised properties remaining after expected ones have been consumed + * + * @param config The configured properties. + * @param prefix The expected prefix for Flyway configuration parameters - or null if none. + */ + public static void checkConfigurationForUnrecognisedProperties(Map config, String prefix) { + ArrayList unknownFlywayProperties = new ArrayList<>(); + for (String key : config.keySet()) { + if (prefix == null || key.startsWith(prefix)) { + unknownFlywayProperties.add(key); + } + } + + if (!unknownFlywayProperties.isEmpty()) { + String property = (unknownFlywayProperties.size() == 1) ? "property" : "properties"; + String message = String.format("Unknown configuration %s: %s", + property, + StringUtils.arrayToCommaDelimitedString(unknownFlywayProperties.toArray())); + throw new FlywayException(message, ErrorCode.CONFIGURATION); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.java new file mode 100644 index 00000000..2ee71104 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.configuration; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; + +public class ConfigurationValidator { + public void validate(Configuration configuration) { + + + + + + + + + + if (configuration.getDataSource() == null) { + throw new FlywayException( + "Unable to connect to the database. Configure the url, user and password!", + ErrorCode.CONFIGURATION); + } + + for (String key : configuration.getPlaceholders().keySet()) { + if (key.toLowerCase().startsWith("flyway:")) { + throw new FlywayException("Invalid placeholder ('flyway:' prefix is reserved): " + key); + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/package-info.java new file mode 100644 index 00000000..19d757ab --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.configuration; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseExecutionStrategy.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseExecutionStrategy.java new file mode 100644 index 00000000..7b8ae63e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseExecutionStrategy.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database; + +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.SQLException; + +/** + * Defines a strategy for executing a {@code SqlCallable} against a particular database. + */ +public interface DatabaseExecutionStrategy { + + /** + * Execute the given callable, using the defined strategy. + * @param callable The SQL call to execute + * @param The return type of the SQL call + * @return The object returned by the SQL call + * @throws SQLException + */ + T execute(final SqlCallable callable) throws SQLException; +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java new file mode 100644 index 00000000..30cbab6b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java @@ -0,0 +1,396 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.cockroachdb.CockroachDBDatabase; +import org.flywaydb.core.internal.database.cockroachdb.CockroachDBParser; +import org.flywaydb.core.internal.database.cockroachdb.CockroachDBRetryingStrategy; +import org.flywaydb.core.internal.database.db2.DB2Database; +import org.flywaydb.core.internal.database.db2.DB2Parser; +import org.flywaydb.core.internal.database.derby.DerbyDatabase; +import org.flywaydb.core.internal.database.derby.DerbyParser; + +import org.flywaydb.core.internal.database.firebird.FirebirdDatabase; +import org.flywaydb.core.internal.database.firebird.FirebirdParser; +import org.flywaydb.core.internal.database.h2.H2Database; +import org.flywaydb.core.internal.database.h2.H2Parser; +import org.flywaydb.core.internal.database.hsqldb.HSQLDBDatabase; +import org.flywaydb.core.internal.database.hsqldb.HSQLDBParser; +import org.flywaydb.core.internal.database.informix.InformixDatabase; +import org.flywaydb.core.internal.database.informix.InformixParser; +import org.flywaydb.core.internal.database.mysql.MySQLDatabase; +import org.flywaydb.core.internal.database.mysql.MySQLParser; +import org.flywaydb.core.internal.database.oracle.OracleDatabase; +import org.flywaydb.core.internal.database.oracle.OracleParser; +import org.flywaydb.core.internal.database.oracle.OracleSqlScriptExecutor; +import org.flywaydb.core.internal.database.postgresql.PostgreSQLDatabase; +import org.flywaydb.core.internal.database.postgresql.PostgreSQLParser; +import org.flywaydb.core.internal.database.redshift.RedshiftDatabase; +import org.flywaydb.core.internal.database.redshift.RedshiftParser; +import org.flywaydb.core.internal.database.saphana.SAPHANADatabase; +import org.flywaydb.core.internal.database.saphana.SAPHANAParser; +import org.flywaydb.core.internal.database.snowflake.SnowflakeDatabase; +import org.flywaydb.core.internal.database.snowflake.SnowflakeParser; +import org.flywaydb.core.internal.database.sqlite.SQLiteDatabase; +import org.flywaydb.core.internal.database.sqlite.SQLiteParser; +import org.flywaydb.core.internal.database.sqlserver.SQLServerDatabase; +import org.flywaydb.core.internal.database.sqlserver.SQLServerParser; +import org.flywaydb.core.internal.database.sybasease.SybaseASEDatabase; +import org.flywaydb.core.internal.database.sybasease.SybaseASEParser; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.*; + +import java.sql.Connection; + +import static org.flywaydb.core.internal.jdbc.DatabaseType.COCKROACHDB; +import static org.flywaydb.core.internal.sqlscript.SqlScriptMetadata.getMetadataResource; + +/** + * Factory for obtaining the correct Database instance for the current connection. + */ +public class DatabaseFactory { + private static final Log LOG = LogFactory.getLog(DatabaseFactory.class); + + /** + * Prevent instantiation. + */ + private DatabaseFactory() { + //Do nothing + } + + /** + * Initializes the appropriate Database class for the database product used by the data source. + * + * @param configuration The Flyway configuration. + * @param printInfo Where the DB info should be printed in the logs. + * @return The appropriate Database class. + */ + public static Database createDatabase(Configuration configuration, boolean printInfo, + JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + OracleDatabase.enableTnsnamesOraSupport(); + + String databaseProductName = jdbcConnectionFactory.getProductName(); + if (printInfo) { + LOG.info("Database: " + jdbcConnectionFactory.getJdbcUrl() + " (" + databaseProductName + ")"); + LOG.debug("Driver : " + jdbcConnectionFactory.getDriverInfo()); + } + + DatabaseType databaseType = jdbcConnectionFactory.getDatabaseType(); + + Database database = createDatabase(databaseType, configuration, jdbcConnectionFactory + + + + ); + + String intendedCurrentSchema = configuration.getDefaultSchema(); + if (!database.supportsChangingCurrentSchema() && intendedCurrentSchema != null) { + LOG.warn(databaseProductName + " does not support setting the schema for the current session. " + + "Default schema will NOT be changed to " + intendedCurrentSchema + " !"); + } + + return database; + } + + private static Database createDatabase(DatabaseType databaseType, Configuration configuration, + JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + switch (databaseType) { + case COCKROACHDB: + return new CockroachDBDatabase(configuration, jdbcConnectionFactory + + + + ); + case DB2: + return new DB2Database(configuration, jdbcConnectionFactory + + + + ); + + + + + + case DERBY: + return new DerbyDatabase(configuration, jdbcConnectionFactory + + + + ); + case FIREBIRD: + return new FirebirdDatabase(configuration, jdbcConnectionFactory + + + + ); + case H2: + return new H2Database(configuration, jdbcConnectionFactory + + + + ); + case HSQLDB: + return new HSQLDBDatabase(configuration, jdbcConnectionFactory + + + + ); + case INFORMIX: + return new InformixDatabase(configuration, jdbcConnectionFactory + + + + ); + case MARIADB: + case MYSQL: + return new MySQLDatabase(configuration, jdbcConnectionFactory + + + + ); + case ORACLE: + return new OracleDatabase(configuration, jdbcConnectionFactory + + + + ); + case POSTGRESQL: + return new PostgreSQLDatabase(configuration, jdbcConnectionFactory + + + + ); + case REDSHIFT: + return new RedshiftDatabase(configuration, jdbcConnectionFactory + + + + ); + case SNOWFLAKE: + return new SnowflakeDatabase(configuration, jdbcConnectionFactory + + + + ); + case SQLITE: + return new SQLiteDatabase(configuration, jdbcConnectionFactory + + + + ); + case SAPHANA: + return new SAPHANADatabase(configuration, jdbcConnectionFactory + + + + ); + case SQLSERVER: + return new SQLServerDatabase(configuration, jdbcConnectionFactory + + + + ); + case SYBASEASE_JCONNECT: + case SYBASEASE_JTDS: + return new SybaseASEDatabase(configuration, jdbcConnectionFactory + + + + ); + default: + throw new FlywayException("Unsupported Database: " + databaseType.name()); + } + } + + public static SqlScriptFactory createSqlScriptFactory(final JdbcConnectionFactory jdbcConnectionFactory, + final Configuration configuration, + final ParsingContext parsingContext) { + final DatabaseType databaseType = jdbcConnectionFactory.getDatabaseType(); + + + + + + + + + + + + + + + + return new SqlScriptFactory() { + @Override + public SqlScript createSqlScript(LoadableResource resource, boolean mixed, ResourceProvider resourceProvider) { + return new ParserSqlScript(createParser(jdbcConnectionFactory, configuration + + + + , parsingContext + ), resource, getMetadataResource(resourceProvider, resource), mixed); + } + }; + + + + } + + private static Parser createParser(JdbcConnectionFactory jdbcConnectionFactory, Configuration configuration + + + + , ParsingContext parsingContext + ) { + final DatabaseType databaseType = jdbcConnectionFactory.getDatabaseType(); + + switch (databaseType) { + case COCKROACHDB: + return new CockroachDBParser(configuration, parsingContext); + case DB2: + return new DB2Parser(configuration, parsingContext); + + + + + case DERBY: + return new DerbyParser(configuration, parsingContext); + case FIREBIRD: + return new FirebirdParser(configuration, parsingContext); + case H2: + return new H2Parser(configuration, parsingContext); + case HSQLDB: + return new HSQLDBParser(configuration, parsingContext); + case INFORMIX: + return new InformixParser(configuration, parsingContext); + case MARIADB: + case MYSQL: + return new MySQLParser(configuration, parsingContext); + case ORACLE: + return new OracleParser(configuration + + + + + + + + + + + , parsingContext + ); + case POSTGRESQL: + return new PostgreSQLParser(configuration, parsingContext); + case REDSHIFT: + return new RedshiftParser(configuration, parsingContext); + case SQLITE: + return new SQLiteParser(configuration, parsingContext); + case SAPHANA: + return new SAPHANAParser(configuration, parsingContext); + case SNOWFLAKE: + return new SnowflakeParser(configuration, parsingContext); + case SQLSERVER: + return new SQLServerParser(configuration, parsingContext); + case SYBASEASE_JCONNECT: + case SYBASEASE_JTDS: + return new SybaseASEParser(configuration, parsingContext); + default: + throw new FlywayException("Unsupported Database: " + databaseType.name()); + } + } + + public static SqlScriptExecutorFactory createSqlScriptExecutorFactory( + final JdbcConnectionFactory jdbcConnectionFactory + + + + + ) { + final DatabaseType databaseType = jdbcConnectionFactory.getDatabaseType(); + + + + + if (DatabaseType.ORACLE == databaseType) { + return new SqlScriptExecutorFactory() { + @Override + public SqlScriptExecutor createSqlScriptExecutor(Connection connection + + + + ) { + return new OracleSqlScriptExecutor(new JdbcTemplate(connection, databaseType) + + + + ); + } + }; + } + + return new SqlScriptExecutorFactory() { + @Override + public SqlScriptExecutor createSqlScriptExecutor(Connection connection + + + + ) { + return new DefaultSqlScriptExecutor(new JdbcTemplate(connection, databaseType) + + + + ); + } + }; + } + + public static DatabaseExecutionStrategy createExecutionStrategy(Connection connection) { + if (connection == null) { + return new DefaultExecutionStrategy(); + } + + DatabaseType databaseType = DatabaseType.fromJdbcConnection(connection); + switch (databaseType) { + case COCKROACHDB: + return new CockroachDBRetryingStrategy(); + default: + return new DefaultExecutionStrategy(); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DefaultExecutionStrategy.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DefaultExecutionStrategy.java new file mode 100644 index 00000000..698e3c3c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DefaultExecutionStrategy.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database; + +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.SQLException; + +/** + * The default execution strategy for a {@code SQLCallable}, which just performs a single execution. + */ +public class DefaultExecutionStrategy implements DatabaseExecutionStrategy { + + public T execute(final SqlCallable callable) throws SQLException { + return callable.call(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Connection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Connection.java new file mode 100644 index 00000000..db556340 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Connection.java @@ -0,0 +1,189 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.JdbcUtils; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; + +import java.io.Closeable; +import java.sql.SQLException; +import java.util.concurrent.Callable; + +public abstract class Connection implements Closeable { + protected final D database; + protected final JdbcTemplate jdbcTemplate; + private final java.sql.Connection jdbcConnection; + + /** + * The original schema of the connection that should be restored later. + */ + protected final String originalSchemaNameOrSearchPath; + + /** + * The original autocommit state of the connection. + */ + private final boolean originalAutoCommit; + + protected Connection(D database, java.sql.Connection connection) { + this.database = database; + + try { + this.originalAutoCommit = connection.getAutoCommit(); + if (!originalAutoCommit) { + connection.setAutoCommit(true); + } + } catch (SQLException e) { + throw new FlywaySqlException("Unable to turn on auto-commit for the connection", e); + } + + this.jdbcConnection = connection; + jdbcTemplate = new JdbcTemplate(jdbcConnection, database.getDatabaseType()); + try { + originalSchemaNameOrSearchPath = getCurrentSchemaNameOrSearchPath(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine the original schema for the connection", e); + } + } + + /** + * Retrieves the current schema. + * + * @return The current schema for this connection. + * @throws SQLException when the current schema could not be retrieved. + */ + protected abstract String getCurrentSchemaNameOrSearchPath() throws SQLException; + + /** + * @return The current schema for this connection. + */ + public final Schema getCurrentSchema() { + try { + return doGetCurrentSchema(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine the current schema for the connection", e); + } + } + + protected Schema doGetCurrentSchema() throws SQLException { + return getSchema(getCurrentSchemaNameOrSearchPath()); + } + + /** + * Retrieves the schema with this name in the database. + * + * @param name The name of the schema. + * @return The schema. + */ + public abstract Schema getSchema(String name); + + /** + * Sets the current schema to this schema. + * + * @param schema The new current schema for this connection. + */ + public void changeCurrentSchemaTo(Schema schema) { + try { + if (!schema.exists()) { + return; + } + doChangeCurrentSchemaOrSearchPathTo(schema.getName()); + } catch (SQLException e) { + throw new FlywaySqlException("Error setting current schema to " + schema, e); + } + } + + /** + * Sets the current schema to this schema. + * + * @param schemaNameOrSearchPath The new current schema for this connection. + * @throws SQLException when the current schema could not be set. + */ + protected void doChangeCurrentSchemaOrSearchPathTo(String schemaNameOrSearchPath) throws SQLException { + } + + /** + * Locks this table and executes this callable. + * + * @param table The table to lock. + * @param callable The callable to execute. + * @return The result of the callable. + */ + public T lock(final Table table, final Callable callable) { + return ExecutionTemplateFactory + .createTableExclusiveExecutionTemplate(jdbcTemplate.getConnection(), table, database) + .execute(callable); + } + + public final JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + @Override + public final void close() { + restoreOriginalState(); + restoreOriginalSchema(); + restoreOriginalAutoCommit(); + JdbcUtils.closeConnection(jdbcConnection); + } + + private void restoreOriginalSchema() { + ExecutionTemplateFactory.createExecutionTemplate(jdbcConnection, database).execute(new Callable() { + @Override + public Void call() { + try { + doChangeCurrentSchemaOrSearchPathTo(originalSchemaNameOrSearchPath); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to restore original schema", e); + } + return null; + } + }); + } + + /** + * Restores this connection to its original state. + */ + public final void restoreOriginalState() { + try { + doRestoreOriginalState(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to restore connection to its original state", e); + } + } + + /** + * Restores this connection to its original auto-commit setting. + */ + private void restoreOriginalAutoCommit() { + try { + jdbcConnection.setAutoCommit(originalAutoCommit); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to restore connection to its original auto-commit setting", e); + } + } + + /** + * Restores this connection to its original state. + */ + protected void doRestoreOriginalState() throws SQLException { + } + + public final java.sql.Connection getJdbcConnection() { + return jdbcConnection; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Database.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Database.java new file mode 100644 index 00000000..b2ca912d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Database.java @@ -0,0 +1,476 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywayDbUpgradeRequiredException; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.license.Edition; +import org.flywaydb.core.internal.license.FlywayEditionUpgradeRequiredException; +import org.flywaydb.core.internal.resource.StringResource; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; +import org.flywaydb.core.internal.util.AbbreviationUtils; + +import java.io.Closeable; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * Abstraction for database-specific functionality. + */ +public abstract class Database implements Closeable { + private static final Log LOG = LogFactory.getLog(Database.class); + + /** + * The type of database this is. + */ + protected final DatabaseType databaseType; + + /** + * The Flyway configuration. + */ + protected final Configuration configuration; + + /** + * The JDBC metadata to use. + */ + protected final DatabaseMetaData jdbcMetaData; + + /** + * The main JDBC connection, without any wrapping. + */ + protected final java.sql.Connection rawMainJdbcConnection; + + /** + * The main connection to use. + */ + private C mainConnection; + + /** + * The connection to use for migrations. + */ + private C migrationConnection; + + protected final JdbcConnectionFactory jdbcConnectionFactory; + + + + + + /** + * The major.minor version of the database. + */ + private MigrationVersion version; + + /** + * The user who applied the migrations. + */ + private String installedBy; + + protected JdbcTemplate jdbcTemplate; + + /** + * Creates a new Database instance with this JdbcTemplate. + * + * @param configuration The Flyway configuration. + */ + public Database(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + this.databaseType = jdbcConnectionFactory.getDatabaseType(); + this.configuration = configuration; + this.rawMainJdbcConnection = jdbcConnectionFactory.openConnection(); + try { + this.jdbcMetaData = rawMainJdbcConnection.getMetaData(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to get metadata for connection", e); + } + this.jdbcTemplate = new JdbcTemplate(rawMainJdbcConnection, databaseType); + this.jdbcConnectionFactory = jdbcConnectionFactory; + + + + } + + /** + * Retrieves a Flyway Connection for this JDBC connection. + * + * @param connection The JDBC connection to wrap. + * @return The Flyway Connection. + */ + private C getConnection(java.sql.Connection connection) { + return doGetConnection(connection); + } + + /** + * Retrieves a Flyway Connection for this JDBC connection. + * + * @param connection The JDBC connection to wrap. + * @return The Flyway Connection. + */ + protected abstract C doGetConnection(java.sql.Connection connection); + + /** + * Ensures Flyway supports this version of this database. + */ + public abstract void ensureSupported(); + + /** + * @return The major.minor version of the database. + */ + public final MigrationVersion getVersion() { + if (version == null) { + version = determineVersion(); + } + return version; + } + + protected final void ensureDatabaseIsRecentEnough(String oldestSupportedVersion) { + if (!getVersion().isAtLeast(oldestSupportedVersion)) { + throw new FlywayDbUpgradeRequiredException(databaseType, computeVersionDisplayName(getVersion()), + computeVersionDisplayName(MigrationVersion.fromVersion(oldestSupportedVersion))); + } + } + + /** + * Ensures this database it at least at recent as this version otherwise suggest upgrade to this higher edition of + * Flyway. + * + * @param oldestSupportedVersionInThisEdition The oldest supported version of the database by this edition of Flyway. + * @param editionWhereStillSupported The edition of Flyway that still supports this version of the database, + * in case it's too old. + */ + protected final void ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition(String oldestSupportedVersionInThisEdition, + Edition editionWhereStillSupported) { + if (!getVersion().isAtLeast(oldestSupportedVersionInThisEdition)) { + throw new FlywayEditionUpgradeRequiredException( + editionWhereStillSupported, + databaseType, + computeVersionDisplayName(getVersion())); + } + } + + protected final void recommendFlywayUpgradeIfNecessary(String newestSupportedVersion) { + if (getVersion().isNewerThan(newestSupportedVersion)) { + recommendFlywayUpgrade(newestSupportedVersion); + } + } + + protected final void recommendFlywayUpgradeIfNecessaryForMajorVersion(String newestSupportedVersion) { + if (getVersion().isMajorNewerThan(newestSupportedVersion)) { + recommendFlywayUpgrade(newestSupportedVersion); + } + } + + private void recommendFlywayUpgrade(String newestSupportedVersion) { + String message = "Flyway upgrade recommended: " + databaseType + " " + computeVersionDisplayName(getVersion()) + + " is newer than this version of Flyway and support has not been tested. " + + "The latest supported version of " + databaseType + " is " + newestSupportedVersion + "."; + + LOG.warn(message); + } + + /** + * Compute the user-friendly display name for this database version. + * + * @return The user-friendly display name. + */ + protected String computeVersionDisplayName(MigrationVersion version) { + return version.getVersion(); + } + + /** + * @return The default delimiter for this database. + */ + public Delimiter getDefaultDelimiter() { + return Delimiter.SEMICOLON; + } + + /** + * @return The current database user. + */ + public final String getCurrentUser() { + try { + return doGetCurrentUser(); + } catch (SQLException e) { + throw new FlywaySqlException("Error retrieving the database user", e); + } + } + + protected String doGetCurrentUser() throws SQLException { + return jdbcMetaData.getUserName(); + } + + /** + * Checks whether DDL transactions are supported by this database. + * + * @return {@code true} if DDL transactions are supported, {@code false} if not. + */ + public abstract boolean supportsDdlTransactions(); + + /** + * Whether to add the baseline marker directly as part of the create table statement for this database. + */ + public boolean useDirectBaseline() { + return false; + } + + /** + * @return {@code true} if this database supports changing a connection's current schema. {@code false if not}. + */ + public abstract boolean supportsChangingCurrentSchema(); + + + + + + + + + + + + + + /** + * @return The representation of the value {@code true} in a boolean column. + */ + public abstract String getBooleanTrue(); + + /** + * @return The representation of the value {@code false} in a boolean column. + */ + public abstract String getBooleanFalse(); + + /** + * Quote these identifiers for use in sql queries. Multiple identifiers will be quoted and separated by a dot. + * + * @param identifiers The identifiers to quote. + * @return The fully qualified quoted identifiers. + */ + public final String quote(String... identifiers) { + StringBuilder result = new StringBuilder(); + + boolean first = true; + for (String identifier : identifiers) { + if (!first) { + result.append("."); + } + first = false; + result.append(doQuote(identifier)); + } + + return result.toString(); + } + + /** + * Quote this identifier for use in sql queries. + * + * @param identifier The identifier to quote. + * @return The fully qualified quoted identifier. + */ + protected abstract String doQuote(String identifier); + + /** + * @return {@code true} if this database use a catalog to represent a schema. {@code false} if a schema is simply a schema. + */ + public abstract boolean catalogIsSchema(); + + /** + * @return Whether to only use a single connection for both schema history table management and applying migrations. + */ + public boolean useSingleConnection() { + return false; + } + + public DatabaseMetaData getJdbcMetaData() { + return jdbcMetaData; + } + + /** + * @return The main connection, used to manipulate the schema history. + */ + public final C getMainConnection() { + if (mainConnection == null) { + this.mainConnection = getConnection(rawMainJdbcConnection); + } + return mainConnection; + } + + /** + * @return The migration connection, used to apply migrations. + */ + public final C getMigrationConnection() { + if (migrationConnection == null) { + if (useSingleConnection()) { + this.migrationConnection = getMainConnection(); + } else { + this.migrationConnection = getConnection(jdbcConnectionFactory.openConnection()); + } + } + return migrationConnection; + } + + /** + * @return The major and minor version of the database. + */ + protected MigrationVersion determineVersion() { + try { + return MigrationVersion.fromVersion(jdbcMetaData.getDatabaseMajorVersion() + "." + jdbcMetaData.getDatabaseMinorVersion()); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine the major version of the database", e); + } + } + + /** + * Retrieves the script used to create the schema history table. + * + * @param table The table to create. + * @param baseline Whether to include the creation of a baseline marker. + * @return The script. + */ + public final SqlScript getCreateScript(SqlScriptFactory sqlScriptFactory, Table table, boolean baseline) { + return sqlScriptFactory.createSqlScript(new StringResource(getRawCreateScript(table, baseline)), false, null); + } + + public abstract String getRawCreateScript(Table table, boolean baseline); + + public String getInsertStatement(Table table) { + return "INSERT INTO " + table + + " (" + quote("installed_rank") + + ", " + quote("version") + + ", " + quote("description") + + ", " + quote("type") + + ", " + quote("script") + + ", " + quote("checksum") + + ", " + quote("installed_by") + + ", " + quote("execution_time") + + ", " + quote("success") + + ")" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + + public final String getBaselineStatement(Table table) { + return String.format(getInsertStatement(table).replace("?", "%s"), + 1, + "'" + configuration.getBaselineVersion() + "'", + "'" + AbbreviationUtils.abbreviateDescription(configuration.getBaselineDescription()) + "'", + "'" + MigrationType.BASELINE + "'", + "'" + AbbreviationUtils.abbreviateScript(configuration.getBaselineDescription()) + "'", + "NULL", + "'" + installedBy + "'", + 0, + getBooleanTrue() + ); + } + + public String getSelectStatement(Table table) { + return "SELECT " + quote("installed_rank") + + "," + quote("version") + + "," + quote("description") + + "," + quote("type") + + "," + quote("script") + + "," + quote("checksum") + + "," + quote("installed_on") + + "," + quote("installed_by") + + "," + quote("execution_time") + + "," + quote("success") + + " FROM " + table + + " WHERE " + quote("installed_rank") + " > ?" + + " ORDER BY " + quote("installed_rank"); + } + + public final String getInstalledBy() { + if (installedBy == null) { + installedBy = configuration.getInstalledBy() == null ? getCurrentUser() : configuration.getInstalledBy(); + } + return installedBy; + } + + public void close() { + if (!useSingleConnection() && migrationConnection != null) { + migrationConnection.close(); + } + if (mainConnection != null) { + mainConnection.close(); + } + } + + public DatabaseType getDatabaseType() { + return databaseType; + } + + /** + * Whether the database supports an empty string as a migration description. + */ + public boolean supportsEmptyMigrationDescription() { return true; } + + /** + * Whether the database supports multi-statement transactions + */ + public boolean supportsMultiStatementTransactions() { return true; } + + /** + * Cleans all the objects in this database that need to be done prior to cleaning schemas. + */ + public void cleanPreSchemas() { + try { + doCleanPreSchemas(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to clean database " + this, e); + } + } + + /** + * Cleans all the objects in this database that need to be done prior to cleaning schemas. + * + * @throws SQLException when the clean failed. + */ + protected void doCleanPreSchemas() throws SQLException { + // Default is to do nothing. + } + + /** + * Cleans all the objects in this database that need to be done after cleaning schemas. + */ + public void cleanPostSchemas() { + try { + doCleanPostSchemas(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to clean schema " + this, e); + } + } + + /** + * Cleans all the objects in this database that need to be done after cleaning schemas. + * + * @throws SQLException when the clean failed. + */ + protected void doCleanPostSchemas() throws SQLException { + // Default is to do nothing + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Function.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Function.java new file mode 100644 index 00000000..1614f8be --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Function.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.util.StringUtils; + +/** + * A user defined type within a schema. + */ +public abstract class Function extends SchemaObject { + /** + * The arguments of the function. + */ + protected String[] args; + + /** + * Creates a new function with this name within this schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this function lives in. + * @param name The name of the function. + * @param args The arguments of the function. + */ + public Function(JdbcTemplate jdbcTemplate, D database, S schema, String name, String... args) { + super(jdbcTemplate, database, schema, name); + this.args = args == null ? new String[0] : args; + } + + @Override + public String toString() { + return super.toString() + "(" + StringUtils.arrayToCommaDelimitedString(args) + ")"; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Schema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Schema.java new file mode 100644 index 00000000..1c8d1273 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Schema.java @@ -0,0 +1,283 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.JdbcUtils; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a database schema. + */ +public abstract class Schema { + private static final Log LOG = LogFactory.getLog(Schema.class); + + /** + * The Jdbc Template for communicating with the DB. + */ + protected final JdbcTemplate jdbcTemplate; + + /** + * The database-specific support. + */ + protected final D database; + + /** + * The name of the schema. + */ + protected final String name; + + /** + * Creates a new schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public Schema(JdbcTemplate jdbcTemplate, D database, String name) { + this.jdbcTemplate = jdbcTemplate; + this.database = database; + this.name = name; + } + + /** + * @return The name of the schema, quoted for the database it lives in. + */ + public String getName() { + return name; + } + + /** + * Checks whether this schema exists. + * + * @return {@code true} if it does, {@code false} if not. + */ + public boolean exists() { + try { + return doExists(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to check whether schema " + this + " exists", e); + } + } + + /** + * Checks whether this schema exists. + * + * @return {@code true} if it does, {@code false} if not. + * @throws SQLException when the check failed. + */ + protected abstract boolean doExists() throws SQLException; + + /** + * Checks whether this schema is empty. + * + * @return {@code true} if it is, {@code false} if isn't. + */ + public boolean empty() { + try { + return doEmpty(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to check whether schema " + this + " is empty", e); + } + } + + /** + * Checks whether this schema is empty. + * + * @return {@code true} if it is, {@code false} if isn't. + * @throws SQLException when the check failed. + */ + protected abstract boolean doEmpty() throws SQLException; + + /** + * Creates this schema in the database. + */ + public void create() { + try { + LOG.info("Creating schema " + this + " ..."); + doCreate(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to create schema " + this, e); + } + } + + /** + * Creates this schema in the database. + * + * @throws SQLException when the creation failed. + */ + protected abstract void doCreate() throws SQLException; + + /** + * Drops this schema from the database. + */ + public void drop() { + try { + doDrop(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to drop schema " + this, e); + } + } + + /** + * Drops this schema from the database. + * + * @throws SQLException when the drop failed. + */ + protected abstract void doDrop() throws SQLException; + + /** + * Cleans all the objects in this schema. + */ + public void clean() { + try { + doClean(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to clean schema " + this, e); + } + } + + /** + * Cleans all the objects in this schema. + * + * @throws SQLException when the clean failed. + */ + protected abstract void doClean() throws SQLException; + + /** + * Retrieves all the tables in this schema. + * + * @return All tables in the schema. + */ + public T[] allTables() { + try { + return doAllTables(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to retrieve all tables in schema " + this, e); + } + } + + /** + * Retrieves all the tables in this schema. + * + * @return All tables in the schema. + * @throws SQLException when the retrieval failed. + */ + protected abstract T[] doAllTables() throws SQLException; + + /** + * Retrieves all the types in this schema. + * + * @return All types in the schema. + */ + protected final Type[] allTypes() { + ResultSet resultSet = null; + try { + resultSet = database.jdbcMetaData.getUDTs(null, name, null, null); + + List types = new ArrayList<>(); + while (resultSet.next()) { + types.add(getType(resultSet.getString("TYPE_NAME"))); + } + + return types.toArray(new Type[0]); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to retrieve all types in schema " + this, e); + } finally { + JdbcUtils.closeResultSet(resultSet); + } + } + + /** + * Retrieves the type with this name in this schema. + * + * @param typeName The name of the type. + * @return The type. + */ + protected Type getType(String typeName) { + return null; + } + + /** + * Retrieves the table with this name in this schema. + * + * @param tableName The name of the table. + * @return The table. + */ + public abstract Table getTable(String tableName); + + /** + * Retrieves the function with this name in this schema. + * + * @param functionName The name of the function. + * @return The function. + */ + public Function getFunction(String functionName, String... args) { + throw new UnsupportedOperationException("getFunction()"); + } + + /** + * Retrieves all the types in this schema. + * + * @return All types in the schema. + */ + protected final Function[] allFunctions() { + try { + return doAllFunctions(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to retrieve all functions in schema " + this, e); + } + } + + /** + * Retrieves all the functions in this schema. + * + * @return All functions in the schema. + * @throws SQLException when the retrieval failed. + */ + protected Function[] doAllFunctions() throws SQLException { + return new Function[0]; + } + + /** + * @return The quoted name of the schema. + */ + @Override + public String toString() { + return database.quote(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Schema schema = (Schema) o; + return name.equals(schema.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/SchemaObject.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/SchemaObject.java new file mode 100644 index 00000000..c9c6134c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/SchemaObject.java @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * An object within a database schema. + */ +public abstract class SchemaObject { + /** + * The Jdbc Template for communicating with the DB. + */ + protected final JdbcTemplate jdbcTemplate; + + /** + * The database-specific support. + */ + protected final D database; + + /** + * The schema this table lives in. + */ + protected final S schema; + + /** + * The name of the table. + */ + protected final String name; + + /** + * Creates a new schema object with this name within this schema. + * + * @param jdbcTemplate The jdbc template to access the DB. + * @param database The database-specific support. + * @param schema The schema the object lives in. + * @param name The name of the object. + */ + SchemaObject(JdbcTemplate jdbcTemplate, D database, S schema, String name) { + this.name = name; + this.jdbcTemplate = jdbcTemplate; + this.database = database; + this.schema = schema; + } + + /** + * @return The schema this object lives in. + */ + public final S getSchema() { + return schema; + } + + /** + * @return The name of the object. + */ + public final String getName() { + return name; + } + + /** + * Drops this object from the database. + */ + public final void drop() { + try { + doDrop(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to drop " + this, e); + } + } + + /** + * Drops this object from the database. + * + * @throws java.sql.SQLException when the drop failed. + */ + protected abstract void doDrop() throws SQLException; + + @Override + public String toString() { + return database.quote(schema.getName(), name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Table.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Table.java new file mode 100644 index 00000000..2f56fe06 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Table.java @@ -0,0 +1,155 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.JdbcUtils; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Represents a database table within a schema. + */ +public abstract class Table extends SchemaObject { + private static final Log LOG = LogFactory.getLog(Table.class); + + /** + * Keep track of the locks on a table. Calls to lock the table can be nested, and also if the table doesn't + * initially exist then we can't lock (and therefore shouldn't unlock either). + */ + protected int lockDepth = 0; + + /** + * Creates a new table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public Table(JdbcTemplate jdbcTemplate, D database, S schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + /** + * Checks whether this table exists. + * + * @return {@code true} if it does, {@code false} if not. + */ + public boolean exists() { + try { + return doExists(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to check whether table " + this + " exists", e); + } + } + + /** + * Checks whether this table exists. + * + * @return {@code true} if it does, {@code false} if not. + * @throws SQLException when the check failed. + */ + protected abstract boolean doExists() throws SQLException; + + /** + * Checks whether the database contains a table matching these criteria. + * + * @param catalog The catalog where the table resides. (optional) + * @param schema The schema where the table resides. (optional) + * @param table The name of the table. (optional) + * @param tableTypes The types of table to look for (ex.: TABLE). (optional) + * @return {@code true} if a matching table has been found, {@code false} if not. + * @throws SQLException when the check failed. + */ + protected boolean exists(Schema catalog, Schema schema, String table, String... tableTypes) throws SQLException { + String[] types = tableTypes; + if (types.length == 0) { + types = null; + } + + ResultSet resultSet = null; + boolean found; + try { + resultSet = database.jdbcMetaData.getTables( + catalog == null ? null : catalog.getName(), + schema == null ? null : schema.getName(), + table, + types); + found = resultSet.next(); + } finally { + JdbcUtils.closeResultSet(resultSet); + } + + return found; + } + + /** + * Locks this table in this schema using a read/write pessimistic lock until the end of the current transaction. + * Note that unlock() still needs to be called even if your database unlocks the table implicitly + * (in which case doUnlock() may be a no-op) in order to maintain the lock count correctly. + */ + public void lock() { + if (!exists()) { + return; + } + try { + doLock(); + lockDepth++; + } catch (SQLException e) { + throw new FlywaySqlException("Unable to lock table " + this, e); + } + } + + /** + * Locks this table in this schema using a read/write pessimistic lock until the end of the current transaction. + * Note that unlock() still needs to be called even if your database unlocks the table implicitly + * (in which case doUnlock() may be a no-op) in order to maintain the lock count correctly. + * @throws SQLException when this table in this schema could not be locked. + */ + protected abstract void doLock() throws SQLException; + + /** + * Unlocks this table in this schema. For databases that require an explicit unlocking, not an implicit + * end-of-transaction one. + */ + public void unlock() { + // lockDepth can be zero if this table didn't exist at the time of the call to lock() + if (!exists() || lockDepth == 0) { + return; + } + try { + doUnlock(); + lockDepth--; + } catch (SQLException e) { + throw new FlywaySqlException("Unable to unlock table " + this, e); + } + } + + /** + * Unlocks this table in this schema. For databases that require an explicit unlocking, not an implicit + * end-of-transaction one. + * + * @throws SQLException when this table in this schema could not be unlocked. + */ + protected void doUnlock() throws SQLException { + // Default behaviour is to do nothing. + }; +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Type.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Type.java new file mode 100644 index 00000000..4fdab065 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Type.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.base; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +/** + * A user defined type within a schema. + */ +public abstract class Type extends SchemaObject { + /** + * Creates a new type with this name within this schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this type lives in. + * @param name The name of the type. + */ + public Type(JdbcTemplate jdbcTemplate, D database, S schema, String name) { + super(jdbcTemplate, database, schema, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/package-info.java new file mode 100644 index 00000000..de4f40e1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.base; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBConnection.java new file mode 100644 index 00000000..db5479c3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBConnection.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; + +/** + * CockroachDB connection. + */ +public class CockroachDBConnection extends Connection { + CockroachDBConnection(CockroachDBDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public Schema getSchema(String name) { + return new CockroachDBSchema(jdbcTemplate, database, name); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SHOW database"); + } + + @Override + public void changeCurrentSchemaTo(Schema schema) { + try { + // Avoid unnecessary schema changes as this trips up CockroachDB + if (schema.getName().equals(originalSchemaNameOrSearchPath) || !schema.exists()) { + return; + } + doChangeCurrentSchemaOrSearchPathTo(schema.getName()); + } catch (SQLException e) { + throw new FlywaySqlException("Error setting current database to " + schema, e); + } + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + if (!StringUtils.hasLength(schema)) { + schema = "DEFAULT"; + } + jdbcTemplate.execute("SET database = " + schema); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBDatabase.java new file mode 100644 index 00000000..fee05082 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBDatabase.java @@ -0,0 +1,155 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * CockroachDB database. + */ +public class CockroachDBDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public CockroachDBDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected CockroachDBConnection doGetConnection(Connection connection) { + return new CockroachDBConnection(this, connection); + } + + + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("1.1"); + recommendFlywayUpgradeIfNecessary("20.1"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL PRIMARY KEY,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INTEGER,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT now(),\n" + + " \"execution_time\" INTEGER NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "CREATE INDEX \"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + @Override + protected MigrationVersion determineVersion() { + String version; + try { + version = getMainConnection().getJdbcTemplate().queryForString("SELECT value FROM crdb_internal.node_build_info where field='Version'"); + if (version == null) { + version = getMainConnection().getJdbcTemplate().queryForString("SELECT value FROM crdb_internal.node_build_info where field='Tag'"); + } + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine CockroachDB version", e); + } + int firstDot = version.indexOf("."); + int majorVersion = Integer.parseInt(version.substring(1, firstDot)); + String minorPatch = version.substring(firstDot + 1); + int minorVersion = Integer.parseInt(minorPatch.substring(0, minorPatch.indexOf("."))); + return MigrationVersion.fromVersion(majorVersion + "." + minorVersion); + } + + public String getDbName() { + return "cockroachdb"; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT * FROM [SHOW SESSION_USER]"); + } + + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + + + + + + + + public String getBooleanTrue() { + return "TRUE"; + } + + public String getBooleanFalse() { + return "FALSE"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + StringUtils.replaceAll(identifier, "\"", "\"\"") + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBParser.java new file mode 100644 index 00000000..704bf4c2 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBParser.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; + +public class CockroachDBParser extends Parser { + public CockroachDBParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '$'; + } + + @SuppressWarnings("Duplicates") + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + String dollarQuote = (char) reader.read() + reader.readUntilIncluding('$'); + reader.swallowUntilExcluding(dollarQuote); + reader.swallow(dollarQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBRetryingStrategy.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBRetryingStrategy.java new file mode 100644 index 00000000..cb61d78a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBRetryingStrategy.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.DatabaseExecutionStrategy; +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.SQLException; + +/** + * CockroachDB recommend the use of retries should we see a SQL error code 40001, which represents a lock wait timeout. + * This class implements an appropriate retry pattern. + */ +public class CockroachDBRetryingStrategy implements DatabaseExecutionStrategy { + private static final Log LOG = LogFactory.getLog(CockroachDBRetryingStrategy.class); + + private static final String DEADLOCK_OR_TIMEOUT_ERROR_CODE = "40001"; + private static final int MAX_RETRIES = 50; + + public T execute(final SqlCallable callable) throws SQLException { + int retryCount = 0; + while (true) { + try { + return callable.call(); + } catch (SQLException e) { + checkRetryOrThrow(e, retryCount); + retryCount++; + } + } + } + + void checkRetryOrThrow(SQLException e, int retryCount) throws SQLException { + if (DEADLOCK_OR_TIMEOUT_ERROR_CODE.equals(e.getSQLState()) && retryCount < MAX_RETRIES) { + LOG.info("Retrying because of deadlock or timeout: " + e.getMessage()); + } + // Exception is non-retryable + throw e; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBSchema.java new file mode 100644 index 00000000..c5d8012f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBSchema.java @@ -0,0 +1,230 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * CockroachDB implementation of Schema. + */ +public class CockroachDBSchema extends Schema { + private static final Log LOG = LogFactory.getLog(CockroachDBSchema.class); + + /** + * Is this CockroachDB 1.x. + */ + final boolean cockroachDB1; + + /** + * Creates a new CockroachDB schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + CockroachDBSchema(JdbcTemplate jdbcTemplate, CockroachDBDatabase database, String name) { + super(jdbcTemplate, database, name); + cockroachDB1 = !database.getVersion().isAtLeast("2"); + } + + @Override + protected boolean doExists() throws SQLException { + return new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Boolean call() throws SQLException { + return doExistsOnce(); + } + }); + } + + private boolean doExistsOnce() throws SQLException { + return jdbcTemplate.queryForBoolean("SELECT EXISTS ( SELECT 1 FROM pg_database WHERE datname=? )", name); + } + + @Override + protected boolean doEmpty() throws SQLException { + return new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Boolean call() throws SQLException { + return doEmptyOnce(); + } + }); + } + + private boolean doEmptyOnce() throws SQLException { + if (cockroachDB1) { + return !jdbcTemplate.queryForBoolean("SELECT EXISTS (" + + " SELECT 1" + + " FROM information_schema.tables" + + " WHERE table_schema=?" + + " AND table_type='BASE TABLE'" + + ")", name); + } + return !jdbcTemplate.queryForBoolean("SELECT EXISTS (" + + " SELECT 1" + + " FROM information_schema.tables " + + " WHERE table_catalog=?" + + " AND table_schema='public'" + + " AND table_type='BASE TABLE'" + + " UNION ALL" + + " SELECT 1" + + " FROM information_schema.sequences " + + " WHERE sequence_catalog=?" + + " AND sequence_schema='public'" + + ")", name, name); + } + + @Override + protected void doCreate() throws SQLException { + new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Integer call() throws SQLException { + doCreateOnce(); + return null; + } + }); + } + + protected void doCreateOnce() throws SQLException { + jdbcTemplate.execute("CREATE DATABASE " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Integer call() throws SQLException { + doDropOnce(); + return null; + } + }); + } + + protected void doDropOnce() throws SQLException { + jdbcTemplate.execute("DROP DATABASE " + database.quote(name)); + } + + @Override + protected void doClean() throws SQLException { + new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Integer call() throws SQLException { + doCleanOnce(); + return null; + } + }); + } + + protected void doCleanOnce() throws SQLException { + for (String statement : generateDropStatementsForViews()) { + jdbcTemplate.execute(statement); + } + + for (Table table : allTables()) { + table.drop(); + } + + for (String statement : generateDropStatementsForSequences()) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generates the statements for dropping the views in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForViews() throws SQLException { + List names = + jdbcTemplate.queryForStringList( + "SELECT table_name FROM information_schema.views" + + " WHERE table_catalog=? AND table_schema='public'", name); + List statements = new ArrayList<>(); + for (String name : names) { + statements.add("DROP VIEW IF EXISTS " + database.quote(this.name, name) + " CASCADE"); + } + + return statements; + } + + /** + * Generates the statements for dropping the sequences in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForSequences() throws SQLException { + List names = + jdbcTemplate.queryForStringList( + "SELECT sequence_name FROM information_schema.sequences" + + " WHERE sequence_catalog=? AND sequence_schema='public'", name); + List statements = new ArrayList<>(); + for (String name : names) { + statements.add("DROP SEQUENCE IF EXISTS " + database.quote(this.name, name) + " CASCADE"); + } + + return statements; + } + + @Override + protected CockroachDBTable[] doAllTables() throws SQLException { + String query; + if (cockroachDB1) { + query = + //Search for all the table names + "SELECT table_name FROM information_schema.tables" + + //in this schema + " WHERE table_schema=?" + + //that are real tables (as opposed to views) + " AND table_type='BASE TABLE'"; + } else { + query = + //Search for all the table names + "SELECT table_name FROM information_schema.tables" + + //in this database + " WHERE table_catalog=?" + + " AND table_schema='public'" + + //that are real tables (as opposed to views) + " AND table_type='BASE TABLE'"; + } + + List tableNames = jdbcTemplate.queryForStringList(query, name); + //Views and child tables are excluded as they are dropped with the parent table when using cascade. + + CockroachDBTable[] tables = new CockroachDBTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new CockroachDBTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new CockroachDBTable(jdbcTemplate, database, this, tableName); + } + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBTable.java new file mode 100644 index 00000000..055eb387 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBTable.java @@ -0,0 +1,160 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.cockroachdb; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.Results; +import org.flywaydb.core.internal.util.SqlCallable; + +import java.math.BigInteger; +import java.sql.SQLException; +import java.util.Random; + +/** + * CockroachDB-specific table. + * + * Note that CockroachDB doesn't support table locks. We therefore use a row in the schema history as a lock indicator; + * if another process ahs inserted such a row we wait (potentially indefinitely) for it to be removed before + * carrying out a migration. + */ +public class CockroachDBTable extends Table { + private static final Log LOG = LogFactory.getLog(CockroachDBTable.class); + + /** + * A random string, used as an ID of this instance of Flyway. + */ + private String tableLockString = RandomStringGenerator.getNextRandomString(); + + /** + * Creates a new CockroachDB table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + CockroachDBTable(JdbcTemplate jdbcTemplate, CockroachDBDatabase database, CockroachDBSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Integer call() throws SQLException { + doDropOnce(); + return null; + } + }); + } + + protected void doDropOnce() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE"); + } + + @Override + protected boolean doExists() throws SQLException { + return new CockroachDBRetryingStrategy().execute(new SqlCallable() { + @Override + public Boolean call() throws SQLException { + return doExistsOnce(); + } + }); + } + + protected boolean doExistsOnce() throws SQLException { + if (schema.cockroachDB1) { + return jdbcTemplate.queryForBoolean("SELECT EXISTS (\n" + + " SELECT 1\n" + + " FROM information_schema.tables \n" + + " WHERE table_schema = ?\n" + + " AND table_name = ?\n" + + ")", schema.getName(), name); + } + + return jdbcTemplate.queryForBoolean("SELECT EXISTS (\n" + + " SELECT 1\n" + + " FROM information_schema.tables \n" + + " WHERE table_catalog = ?\n" + + " AND table_schema = 'public'\n" + + " AND table_name = ?\n" + + ")", schema.getName(), name); + } + + @Override + protected void doLock() throws SQLException { + if (lockDepth > 0) { + // Lock has already been taken - so the relevant row in the table already exists + return; + } + + int retryCount = 0; + do { + try { + if (insertLockingRow()) { + return; + } + retryCount++; + LOG.debug("Waiting for lock on " + this); + Thread.sleep(1000); + } catch (InterruptedException ex) { + // Ignore - if interrupted, we still need to wait for lock to become available + } + } while (retryCount < 50); + + throw new FlywayException("Unable to obtain table lock - another Flyway instance may be running"); + } + + private boolean insertLockingRow() { + // Insert the locking row - the primary keyness of installed_rank will prevent us having two. + Results results = jdbcTemplate.executeStatement("INSERT INTO " + this + " VALUES (-100, '" + tableLockString + "', 'flyway-lock', '', '', 0, '', now(), 0, TRUE)"); + // Succeeded if one row updated and no errors. + return (results.getResults().size() > 0 + && results.getResults().get(0).getUpdateCount() == 1 + && results.getErrors().size() == 0); + } + + @Override + protected void doUnlock() throws SQLException { + // Leave the locking row alone until we get to the final level of unlocking + if (lockDepth > 1) { + return; + } + + // Check that there are no other locks in place. This should not happen! + int competingLocksTaken = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM " + this + " WHERE version != '" + tableLockString + "' AND DESCRIPTION = 'flyway-lock'"); + if (competingLocksTaken > 0) { + throw new FlywayException("Internal error: on unlocking, a competing lock was found"); + } + + // Remove the locking row + jdbcTemplate.executeStatement("DELETE FROM " + this + " WHERE version = '" + tableLockString + "' AND DESCRIPTION = 'flyway-lock'"); + } +} + +class RandomStringGenerator { + static final Random random = new Random(); + + //get next random string + public static String getNextRandomString(){ + BigInteger bInt = new BigInteger(128, random); + return bInt.toString(16); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/package-info.java new file mode 100644 index 00000000..d41e21dc --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.cockroachdb; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Connection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Connection.java new file mode 100644 index 00000000..6a53927f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Connection.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +/** + * DB2 connection. + */ +public class DB2Connection extends Connection { + DB2Connection(DB2Database database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("select current_schema from sysibm.sysdummy1"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SET SCHEMA " + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new DB2Schema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Database.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Database.java new file mode 100644 index 00000000..f7f2697b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Database.java @@ -0,0 +1,150 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * DB2 database. + */ +public class DB2Database extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public DB2Database(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected DB2Connection doGetConnection(Connection connection) { + return new DB2Connection(this, connection); + } + + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("9.7"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("11.1", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("11.5"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = configuration.getTablespace() == null + ? "" + : " IN \"" + configuration.getTablespace() + "\""; + + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP DEFAULT CURRENT TIMESTAMP NOT NULL,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" SMALLINT NOT NULL,\n" + + " CONSTRAINT \"" + table.getName() + "_s\" CHECK (\"success\" in(0,1))\n" + + ")" + + + + + " ORGANIZE BY ROW" + + + + + tablespace + ";\n" + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");\n" + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");" + + (baseline ? getBaselineStatement(table) + ";\n" : ""); + } + + @Override + public String getSelectStatement(Table table) { + return super.getSelectStatement(table) + // Allow uncommitted reads so info can be invoked while migrate is running + + " WITH UR"; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("select CURRENT_USER from sysibm.sysdummy1"); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return false; + } + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Function.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Function.java new file mode 100644 index 00000000..e14bc0e7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Function.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Function; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * DB2-specific function. + */ +public class DB2Function extends Function { + /** + * Creates a new Db2 function. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this function lives in. + * @param name The name of the function. + * @param args The arguments of the function. + */ + DB2Function(JdbcTemplate jdbcTemplate, Database database, Schema schema, String name, String... args) { + super(jdbcTemplate, database, schema, name, args); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SPECIFIC FUNCTION " + database.quote(schema.getName(), name)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Parser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Parser.java new file mode 100644 index 00000000..668a74fb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Parser.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class DB2Parser extends Parser { + private static final String COMMENT_DIRECTIVE = "--#"; + private static final String SET_TERMINATOR_DIRECTIVE = COMMENT_DIRECTIVE + "SET TERMINATOR "; + + public DB2Parser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, COMMENT_DIRECTIVE.length()); + } + + // WHILE and FOR both contain DO before the body of the block, so are both handled by the DO keyword + // See https://www.ibm.com/support/knowledgecenter/en/SSEPEK_10.0.0/sqlref/src/tpc/db2z_sqlplnativeintro.html + private static final List CONTROL_FLOW_KEYWORDS = Arrays.asList("LOOP", "CASE", "DO", "REPEAT", "IF"); + + private static final Pattern CREATE_IF_NOT_EXISTS = Pattern.compile( + ".*CREATE\\s([^\\s]+\\s){0,2}IF\\sNOT\\sEXISTS"); + private static final Pattern DROP_IF_EXISTS = Pattern.compile( + ".*DROP\\s([^\\s]+\\s){0,2}IF\\sEXISTS"); + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + boolean previousTokenIsKeyword = !tokens.isEmpty() && tokens.get(tokens.size() - 1).getType() == TokenType.KEYWORD; + + int lastKeywordIndex = getLastKeywordIndex(tokens); + String previousKeyword = lastKeywordIndex >= 0 ? tokens.get(lastKeywordIndex).getText() : null; + + lastKeywordIndex = getLastKeywordIndex(tokens, lastKeywordIndex); + String previousPreviousToken = lastKeywordIndex >= 0 ? tokens.get(lastKeywordIndex).getText() : null; + + if ( + // BEGIN increases block depth, exception when used with ROW BEGIN + ("BEGIN".equals(keyword.getText()) && (!"ROW".equals(previousKeyword) || previousPreviousToken == null || "EACH".equals(previousPreviousToken))) + // Control flow keywords increase depth + || CONTROL_FLOW_KEYWORDS.contains(keyword.getText()) + ) { + // But not END IF and END WHILE + if (!previousTokenIsKeyword || !"END".equals(previousKeyword)) { + context.increaseBlockDepth(); + + } + } else if ( + // END decreases block depth, exception when used with ROW END + ("END".equals(keyword.getText()) && !"ROW".equals(previousKeyword)) + || doTokensMatchPattern(tokens, keyword, CREATE_IF_NOT_EXISTS) + || doTokensMatchPattern(tokens, keyword, DROP_IF_EXISTS)) { + context.decreaseBlockDepth(); + } + } + + @Override + protected void resetDelimiter(ParserContext context) { + // Do not reset delimiter as delimiter changes survive beyond a single statement + } + + @Override + protected boolean isCommentDirective(String peek) { + return peek.startsWith(COMMENT_DIRECTIVE); + } + + @Override + protected Token handleCommentDirective(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + if (SET_TERMINATOR_DIRECTIVE.equals(reader.peek(SET_TERMINATOR_DIRECTIVE.length()))) { + reader.swallow(SET_TERMINATOR_DIRECTIVE.length()); + String delimiter = reader.readUntilExcluding('\n', '\r'); + return new Token(TokenType.NEW_DELIMITER, pos, line, col, delimiter.trim(), delimiter, context.getParensDepth()); + } + reader.swallowUntilExcluding('\n', '\r'); + return new Token(TokenType.COMMENT, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Schema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Schema.java new file mode 100644 index 00000000..00d36a22 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Schema.java @@ -0,0 +1,312 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.internal.database.base.Function; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.database.base.Type; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * DB2 implementation of Schema. + */ +public class DB2Schema extends Schema { + /** + * Creates a new DB2 schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + DB2Schema(JdbcTemplate jdbcTemplate, DB2Database database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT count(*) from (" + + "SELECT 1 FROM syscat.schemata WHERE schemaname=?" + + ")", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + return jdbcTemplate.queryForInt("select count(*) from (" + + "select 1 from syscat.tables where tabschema = ? " + + "union " + + "select 1 from syscat.views where viewschema = ? " + + "union " + + "select 1 from syscat.sequences where seqschema = ? " + + "union " + + "select 1 from syscat.indexes where indschema = ? " + + "union " + + "select 1 from syscat.routines where ROUTINESCHEMA = ? " + + "union " + + "select 1 from syscat.triggers where trigschema = ? " + + ")", name, name, name, name, name, name) == 0; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + clean(); + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " RESTRICT"); + } + + @Override + protected void doClean() throws SQLException { + // MQTs are dropped when the backing views or tables are dropped + // Indexes in DB2 are dropped when the corresponding table is dropped + + + + + // drop versioned table link -> not supported for DB2 9.x + List dropVersioningStatements = generateDropVersioningStatement(); + if (!dropVersioningStatements.isEmpty()) { + // Do a explicit drop of MQTs in order to be able to drop the Versioning + for (String dropTableStatement : generateDropStatements("S", "TABLE")) { + jdbcTemplate.execute(dropTableStatement); + } + } + + for (String dropVersioningStatement : dropVersioningStatements) { + jdbcTemplate.execute(dropVersioningStatement); + } + + + + + // views + for (String dropStatement : generateDropStatementsForViews()) { + jdbcTemplate.execute(dropStatement); + } + + // aliases + for (String dropStatement : generateDropStatements("A", "ALIAS")) { + jdbcTemplate.execute(dropStatement); + } + + // temporary Tables + for (String dropStatement : generateDropStatements("G", "TABLE")) { + jdbcTemplate.execute(dropStatement); + } + + for (Table table : allTables()) { + table.drop(); + } + + // sequences + for (String dropStatement : generateDropStatementsForSequences()) { + jdbcTemplate.execute(dropStatement); + } + + // procedures + for (String dropStatement : generateDropStatementsForProcedures()) { + jdbcTemplate.execute(dropStatement); + } + + // triggers + for (String dropStatement : generateDropStatementsForTriggers()) { + jdbcTemplate.execute(dropStatement); + } + + // modules + for (String dropStatement : generateDropStatementsForModules()) { + jdbcTemplate.execute(dropStatement); + } + + for (Function function : allFunctions()) { + function.drop(); + } + + for (Type type : allTypes()) { + type.drop(); + } + } + + /** + * Generates DROP statements for the procedures in this schema. + * + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatementsForProcedures() throws SQLException { + String dropProcGenQuery = + "select SPECIFICNAME from SYSCAT.ROUTINES where ROUTINETYPE='P' and ROUTINESCHEMA = '" + name + "'"; + return buildDropStatements("DROP SPECIFIC PROCEDURE", dropProcGenQuery); + } + + /** + * Generates DROP statements for the triggers in this schema. + * + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatementsForTriggers() throws SQLException { + String dropTrigGenQuery = "select TRIGNAME from SYSCAT.TRIGGERS where TRIGSCHEMA = '" + name + "'"; + return buildDropStatements("DROP TRIGGER", dropTrigGenQuery); + } + + /** + * Generates DROP statements for the sequences in this schema. + * + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatementsForSequences() throws SQLException { + String dropSeqGenQuery = "select SEQNAME from SYSCAT.SEQUENCES where SEQSCHEMA = '" + name + + "' and SEQTYPE='S'"; + return buildDropStatements("DROP SEQUENCE", dropSeqGenQuery); + } + + /** + * Generates DROP statements for the views in this schema. + * + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatementsForViews() throws SQLException { + String dropSeqGenQuery = "select TABNAME from SYSCAT.TABLES where TYPE='V' AND TABSCHEMA = '" + name + "'" + + + + + + // Filter out statistical view for an index with an expression-based key + // See https://www.ibm.com/support/knowledgecenter/SSEPGG_10.5.0/com.ibm.db2.luw.sql.ref.doc/doc/r0001063.html + " and substr(property,19,1) <> 'Y'" + + + + ; + + return buildDropStatements("DROP VIEW", dropSeqGenQuery); + } + + private List generateDropStatementsForModules() throws SQLException { + String dropSeqGenQuery = + "select MODULENAME from syscat.modules where MODULESCHEMA = '" + + name + + "' and OWNERTYPE='U'"; + + + return buildDropStatements("DROP MODULE", dropSeqGenQuery); + } + + /** + * Generates DROP statements for this type of table, representing this type of object in this schema. + * + * @param tableType The type of table (Can be T, V, S, ...). + * @param objectType The type of object. + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatements(String tableType, String objectType) throws SQLException { + String dropTablesGenQuery = "select TABNAME from SYSCAT.TABLES where TYPE='" + tableType + "' and TABSCHEMA = '" + + name + "'"; + return buildDropStatements("DROP " + objectType, dropTablesGenQuery); + } + + /** + * Builds the drop statements for database objects in this schema. + * + * @param dropPrefix The drop command for the database object (e.g. 'drop table'). + * @param query The query to get all present database objects + * @return The statements. + * @throws SQLException when the drop statements could not be built. + */ + private List buildDropStatements(final String dropPrefix, final String query) throws SQLException { + List dropStatements = new ArrayList<>(); + List dbObjects = jdbcTemplate.queryForStringList(query); + for (String dbObject : dbObjects) { + dropStatements.add(dropPrefix + " " + database.quote(name, dbObject)); + } + return dropStatements; + } + + /** + * @return All tables that have versioning associated with them. + */ + private List generateDropVersioningStatement() throws SQLException { + List dropVersioningStatements = new ArrayList<>(); + Table[] versioningTables = findTables("select TABNAME from SYSCAT.TABLES where TEMPORALTYPE <> 'N' and TABSCHEMA = ?", name); + for (Table table : versioningTables) { + dropVersioningStatements.add("ALTER TABLE " + table.toString() + " DROP VERSIONING"); + } + + return dropVersioningStatements; + } + + private DB2Table[] findTables(String sqlQuery, String... params) throws SQLException { + List tableNames = jdbcTemplate.queryForStringList(sqlQuery, params); + DB2Table[] tables = new DB2Table[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new DB2Table(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + protected DB2Table[] doAllTables() throws SQLException { + return findTables("select TABNAME from SYSCAT.TABLES where TYPE='T' and TABSCHEMA = ?", name); + } + + @Override + protected Function[] doAllFunctions() throws SQLException { + List functionNames = jdbcTemplate.queryForStringList( + "select SPECIFICNAME from SYSCAT.ROUTINES where" + // Functions only + + " ROUTINETYPE='F'" + // That aren't system-generated or built-in + + " AND ORIGIN IN (" + + "'E', " // User-defined, external + + "'M', " // Template function + + "'Q', " // SQL-bodied + + "'U')" // User-defined, based on a source + + " and ROUTINESCHEMA = ?", name); + + List functions = new ArrayList<>(); + for (String functionName : functionNames) { + functions.add(getFunction(functionName)); + } + + return functions.toArray(new Function[0]); + } + + @Override + public Table getTable(String tableName) { + return new DB2Table(jdbcTemplate, database, this, tableName); + } + + @Override + protected Type getType(String typeName) { + return new DB2Type(jdbcTemplate, database, this, typeName); + } + + @Override + public Function getFunction(String functionName, String... args) { + return new DB2Function(jdbcTemplate, database, this, functionName, args); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Table.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Table.java new file mode 100644 index 00000000..e6e95633 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Table.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Db2-specific table. + */ +public class DB2Table extends Table { + /** + * Creates a new Db2 table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + DB2Table(JdbcTemplate jdbcTemplate, DB2Database database, DB2Schema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + this); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.update("lock table " + this + " in exclusive mode"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Type.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Type.java new file mode 100644 index 00000000..b1ac524b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Type.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.db2; + +import org.flywaydb.core.internal.database.base.Type; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Db2-specific type. + */ +public class DB2Type extends Type { + /** + * Creates a new Db2 type. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this type lives in. + * @param name The name of the type. + */ + DB2Type(JdbcTemplate jdbcTemplate, DB2Database database, DB2Schema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TYPE " + database.quote(schema.getName(), name)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/package-info.java new file mode 100644 index 00000000..c26c8b06 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.db2; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyConnection.java new file mode 100644 index 00000000..f4c4a2bb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyConnection.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.derby; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +/** + * Derby connection. + */ +public class DerbyConnection extends Connection { + DerbyConnection(DerbyDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT CURRENT SCHEMA FROM SYSIBM.SYSDUMMY1"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SET SCHEMA " + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new DerbySchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyDatabase.java new file mode 100644 index 00000000..96ba0815 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyDatabase.java @@ -0,0 +1,128 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.derby; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Derby database. + */ +public class DerbyDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public DerbyDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected DerbyConnection doGetConnection(Connection connection) { + return new DerbyConnection(this, connection); + } + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("10.11.1.1"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("10.13", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("10.15"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");\n" + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT CURRENT_USER FROM SYSIBM.SYSDUMMY1"); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "true"; + } + + @Override + public String getBooleanFalse() { + return "false"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyParser.java new file mode 100644 index 00000000..da6d1c0f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyParser.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.derby; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; + +public class DerbyParser extends Parser { + public DerbyParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '$'; + } + + @SuppressWarnings("Duplicates") + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + reader.swallowUntilExcluding("$$"); + reader.swallow(2); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbySchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbySchema.java new file mode 100644 index 00000000..99a1a53b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbySchema.java @@ -0,0 +1,163 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.derby; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Derby implementation of Schema. + */ +public class DerbySchema extends Schema { + /** + * Creates a new Derby schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public DerbySchema(JdbcTemplate jdbcTemplate, DerbyDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT (*) FROM sys.sysschemas WHERE schemaname=?", name) > 0; + } + + @Override + protected boolean doEmpty() { + return allTables().length == 0; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + clean(); + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " RESTRICT"); + } + + @Override + protected void doClean() throws SQLException { + List triggerNames = listObjectNames("TRIGGER", ""); + for (String statement : generateDropStatements("TRIGGER", triggerNames, "")) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForConstraints()) { + jdbcTemplate.execute(statement); + } + + List viewNames = listObjectNames("TABLE", "TABLETYPE='V'"); + for (String statement : generateDropStatements("VIEW", viewNames, "")) { + jdbcTemplate.execute(statement); + } + + for (Table table : allTables()) { + table.drop(); + } + + List sequenceNames = listObjectNames("SEQUENCE", ""); + for (String statement : generateDropStatements("SEQUENCE", sequenceNames, "RESTRICT")) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generate the statements for dropping all the constraints in this schema. + * + * @return The list of statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatementsForConstraints() throws SQLException { + List> results = jdbcTemplate.queryForList("SELECT c.constraintname, t.tablename FROM sys.sysconstraints c" + + " INNER JOIN sys.systables t ON c.tableid = t.tableid" + + " INNER JOIN sys.sysschemas s ON c.schemaid = s.schemaid" + + " WHERE c.type = 'F' AND s.schemaname = ?", name); + + List statements = new ArrayList<>(); + for (Map result : results) { + String dropStatement = "ALTER TABLE " + database.quote(name, result.get("TABLENAME")) + + " DROP CONSTRAINT " + database.quote(result.get("CONSTRAINTNAME")); + + statements.add(dropStatement); + } + return statements; + } + + /** + * Generate the statements for dropping all the objects of this type in this schema. + * + * @param objectType The type of object to drop (Sequence, constant, ...) + * @param objectNames The names of the objects to drop. + * @param dropStatementSuffix Suffix to append to the statement for dropping the objects. + * @return The list of statements. + */ + private List generateDropStatements(String objectType, List objectNames, String dropStatementSuffix) { + List statements = new ArrayList<>(); + for (String objectName : objectNames) { + String dropStatement = + "DROP " + objectType + " " + database.quote(name, objectName) + " " + dropStatementSuffix; + + statements.add(dropStatement); + } + return statements; + } + + @Override + protected DerbyTable[] doAllTables() throws SQLException { + List tableNames = listObjectNames("TABLE", "TABLETYPE='T'"); + + DerbyTable[] tables = new DerbyTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new DerbyTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + /** + * List the names of the objects of this type in this schema. + * + * @param objectType The type of objects to list (Sequence, constant, ...) + * @param querySuffix Suffix to append to the query to find the objects to list. + * @return The names of the objects. + * @throws SQLException when the object names could not be listed. + */ + private List listObjectNames(String objectType, String querySuffix) throws SQLException { + String query = "SELECT " + objectType + "name FROM sys.sys" + objectType + "s WHERE schemaid in (SELECT schemaid FROM sys.sysschemas where schemaname = ?)"; + if (StringUtils.hasLength(querySuffix)) { + query += " AND " + querySuffix; + } + + return jdbcTemplate.queryForStringList(query, name); + } + + @Override + public Table getTable(String tableName) { + return new DerbyTable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyTable.java new file mode 100644 index 00000000..b639016c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyTable.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.derby; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Derby-specific table. + */ +public class DerbyTable extends Table { + /** + * Creates a new Derby table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public DerbyTable(JdbcTemplate jdbcTemplate, DerbyDatabase database, DerbySchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name)); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("LOCK TABLE " + this + " IN EXCLUSIVE MODE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/package-info.java new file mode 100644 index 00000000..4b06743d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.derby; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdConnection.java new file mode 100644 index 00000000..e9ceab24 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdConnection.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.firebird; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +public class FirebirdConnection extends Connection { + + private static final String DUMMY_SCHEMA_NAME = "default"; + + FirebirdConnection(FirebirdDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return DUMMY_SCHEMA_NAME; + } + + @Override + public Schema getSchema(String name) { + // database == schema, always return the same dummy schema + return new FirebirdSchema(jdbcTemplate, database, DUMMY_SCHEMA_NAME); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdDatabase.java new file mode 100644 index 00000000..b10c32c0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdDatabase.java @@ -0,0 +1,138 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.firebird; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +public class FirebirdDatabase extends Database { + /** + * Creates a new FirebirdDatabase instance with this JdbcTemplate. + * + * @param configuration The Flyway configuration. + */ + public FirebirdDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected FirebirdConnection doGetConnection(Connection connection) { + return new FirebirdConnection( this, connection); + } + + + + + + + + + + + + @Override + public void ensureSupported() { + ensureDatabaseIsRecentEnough("3.0"); + } + + @Override + public boolean supportsDdlTransactions() { + // but can't use DDL changes in DML in same transaction + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + // one schema, can't be changed + return false; + } + + @Override + public String getBooleanTrue() { + // boolean datatype introduced in Firebird 3, but this allows broader support + return "1"; + } + + @Override + public String getBooleanFalse() { + // boolean datatype introduced in Firebird 3, but this allows broader support + return "0"; + } + + @Override + protected String doQuote(String identifier) { + // escape double quote in identifier name + return '"' + identifier.replace("\"", "\"\"") + "\""; + } + + @Override + public boolean catalogIsSchema() { + // database == schema + return true; + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String createScript = "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INTEGER CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INTEGER,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n" + + " \"execution_time\" INTEGER NOT NULL,\n" + + " \"success\" SMALLINT NOT NULL\n" + + ");\n" + + "CREATE INDEX \"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");\n"; + + if (baseline) { + // COMMIT RETAIN is needed to be able to insert into the created table. + // This will commit the transaction, but reuse the transaction handle so the JDBC driver doesn't break with + // an "invalid transaction handle" error. + createScript += "COMMIT RETAIN;\n" + + getBaselineStatement(table) + ";\n"; + + } + + return createScript; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + // JDBC DatabaseMetaData.getUserName() reports original user used for connecting, but this may be remapped + return getMainConnection().getJdbcTemplate().queryForString("select CURRENT_USER from RDB$DATABASE"); + } + + @Override + public boolean useSingleConnection() { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdParser.java new file mode 100644 index 00000000..4dc64a25 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdParser.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.firebird; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; + +public class FirebirdParser extends Parser { + + private static final String TERM_WITH_SPACES = " TERM "; + + public FirebirdParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected Token handleKeyword(PeekingReader reader, ParserContext context, int pos, int line, int col, String keyword) throws IOException { + if (keywordIs("SET", keyword)) { + // Try to detect if this is set SET TERM + String possiblyTerm = reader.peek(TERM_WITH_SPACES.length()); + if (keywordIs(TERM_WITH_SPACES, possiblyTerm)) { + reader.swallow(TERM_WITH_SPACES.length()); + String newDelimiter = reader.readUntilExcluding(context.getDelimiter().getDelimiter()); + reader.swallow(context.getDelimiter().getDelimiter().length()); + return new Token(TokenType.NEW_DELIMITER, pos, line, col, newDelimiter.trim(), newDelimiter, context.getParensDepth()); + } + } + return super.handleKeyword(reader, context, pos, line, col, keyword); + } + + @Override + protected void resetDelimiter(ParserContext context) { + // Do not reset delimiter as delimiter changes survive beyond a single statement + } + + @Override + protected boolean isAlternativeStringLiteral(String peek) { + // Support Firebird 3+ Q-quoted string + if (peek.length() < 3) { + return false; + } + char firstChar = peek.charAt(0); + return (firstChar == 'q' || firstChar == 'Q') && peek.charAt(1) == '\''; + } + + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + String closeQuote = computeAlternativeCloseQuote((char) reader.read()); + reader.swallowUntilExcluding(closeQuote); + reader.swallow(closeQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + private String computeAlternativeCloseQuote(char specialChar) { + switch (specialChar) { + case '[': + return "]'"; + case '(': + return ")'"; + case '{': + return "}'"; + case '<': + return ">'"; + default: + return specialChar + "'"; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdSchema.java new file mode 100644 index 00000000..4292588d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdSchema.java @@ -0,0 +1,265 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.firebird; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class FirebirdSchema extends Schema { + /** + * Creates a new Firebird schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public FirebirdSchema(JdbcTemplate jdbcTemplate, FirebirdDatabase database, String name) { + super(jdbcTemplate, database, name); + + } + + @Override + protected boolean doExists() throws SQLException { + // database == schema, always return true + return true; + } + + @Override + protected boolean doEmpty() throws SQLException { + // database == schema, check content of database + // Check for all object types except custom collations and roles + return 0 == jdbcTemplate.queryForInt("select count(*)\n" + + "from (\n" + + " -- views and tables\n" + + " select RDB$RELATION_NAME AS OBJECT_NAME\n" + + " from RDB$RELATIONS\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- stored procedures\n" + + " select RDB$PROCEDURE_NAME\n" + + " from RDB$PROCEDURES\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- triggers\n" + + " select RDB$TRIGGER_NAME\n" + + " from RDB$TRIGGERS\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- functions\n" + + " select RDB$FUNCTION_NAME\n" + + " from RDB$FUNCTIONS\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- sequences\n" + + " select RDB$GENERATOR_NAME\n" + + " from RDB$GENERATORS\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- exceptions\n" + + " select RDB$EXCEPTION_NAME\n" + + " from RDB$EXCEPTIONS\n" + + " where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + " union all\n" + + " -- domains\n" + + " select RDB$FIELD_NAME\n" + + " from RDB$FIELDS\n" + + " where RDB$FIELD_NAME not starting with 'RDB$'\n" + + " and (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n" + + "union all\n" + + "-- packages\n" + + "select RDB$PACKAGE_NAME\n" + + "from RDB$PACKAGES\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)) a"); + } + + @Override + protected void doCreate() throws SQLException { + // database == schema, do nothing for creation + } + + @Override + protected void doDrop() throws SQLException { + // database == schema, doClean() instead + doClean(); + } + + @Override + protected void doClean() throws SQLException { + // Dropping everything except custom collations and roles + for (String dropPackageStmt : generateDropPackageStatements()) { + jdbcTemplate.execute(dropPackageStmt); + } + for (String dropProcedureStmt : generateDropProcedureStatements()) { + jdbcTemplate.execute(dropProcedureStmt); + } + for (String dropViewStmt : generateDropViewStatements()) { + jdbcTemplate.execute(dropViewStmt); + } + + for (String dropConstraintStmt: generateDropConstraintStatements()) { + jdbcTemplate.execute(dropConstraintStmt); + } + + for (Table table : allTables()) { + table.drop(); + } + for (String dropTriggerStmt : generateDropTriggerStatements()) { + jdbcTemplate.execute(dropTriggerStmt); + } + for (String dropFunctionStmt : generateDropFunctionStatements()) { + jdbcTemplate.execute(dropFunctionStmt); + } + for (String dropSequenceStmt : generateDropSequenceStatements()) { + jdbcTemplate.execute(dropSequenceStmt); + } + for (String dropExceptionStmt : generateDropExceptionStatements()) { + jdbcTemplate.execute(dropExceptionStmt); + } + for (String dropDomainStmt : generateDropDomainStatements()) { + jdbcTemplate.execute(dropDomainStmt); + } + } + + private List generateDropConstraintStatements() throws SQLException { + return jdbcTemplate.query( + "select RDB$RELATION_NAME, RDB$CONSTRAINT_NAME\n" + + "from RDB$RELATION_CONSTRAINTS\n" + + "where RDB$RELATION_NAME NOT LIKE 'RDB$%'\n" + + "and RDB$CONSTRAINT_TYPE='FOREIGN KEY'", + new RowMapper() { + @Override + public String mapRow(ResultSet rs) throws SQLException { + String tableName = rs.getString(1); + String constraintName = rs.getString(2); + return "ALTER TABLE " + tableName + " DROP CONSTRAINT " + constraintName; + } + }); + } + + private List generateDropPackageStatements() throws SQLException { + List packageNames = jdbcTemplate.queryForStringList( + "select RDB$PACKAGE_NAME as packageName\n" + + "from RDB$PACKAGES\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)"); + + return generateDropStatements("package", packageNames); + } + + private List generateDropProcedureStatements() throws SQLException { + List procedureNames = jdbcTemplate.queryForStringList( + "select RDB$PROCEDURE_NAME as procedureName\n" + + "from RDB$PROCEDURES\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)" + + "\nand RDB$PACKAGE_NAME is null"); + + return generateDropStatements("procedure", procedureNames); + } + + private List generateDropViewStatements() throws SQLException { + List viewNames = jdbcTemplate.queryForStringList( + "select RDB$RELATION_NAME as viewName\n" + + "from RDB$RELATIONS\n" + + "where RDB$VIEW_BLR is not null\n" + + "and (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)"); + + return generateDropStatements("view", viewNames); + } + + private List generateDropTriggerStatements() throws SQLException { + List triggerNames = jdbcTemplate.queryForStringList( + "select RDB$TRIGGER_NAME as triggerName\n" + + "from RDB$TRIGGERS\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n"); + + return generateDropStatements("trigger", triggerNames); + } + + private List generateDropFunctionStatements() throws SQLException { + List functionNames = jdbcTemplate.queryForStringList( + "select RDB$FUNCTION_NAME as functionName\n" + + "from RDB$FUNCTIONS\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)"); + + String functionTypeName = database.getVersion().isAtLeast("3.0") + ? "function" + : "external function"; + return generateDropStatements(functionTypeName, functionNames); + } + + private List generateDropSequenceStatements() throws SQLException { + List sequenceNames = jdbcTemplate.queryForStringList( + "select RDB$GENERATOR_NAME as sequenceName\n" + + "from RDB$GENERATORS\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n"); + + return generateDropStatements("sequence", sequenceNames); + } + + private List generateDropExceptionStatements() throws SQLException { + List exceptionNames = jdbcTemplate.queryForStringList( + "select RDB$EXCEPTION_NAME as exceptionName\n" + + "from RDB$EXCEPTIONS\n" + + "where (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n"); + + return generateDropStatements("exception", exceptionNames); + } + + private List generateDropDomainStatements() throws SQLException { + List domainNames = jdbcTemplate.queryForStringList( + "select RDB$FIELD_NAME as domainName\n" + + "from RDB$FIELDS\n" + + "where RDB$FIELD_NAME not starting with 'RDB$'\n" + + "and (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)\n"); + + return generateDropStatements("domain", domainNames); + } + + private List generateDropStatements(String objectType, List objectNames) { + List statements = new ArrayList<>(objectNames.size()); + for (String objectName : objectNames) { + statements.add("drop " + objectType + " " + database.quote(objectName)); + } + return statements; + } + + @Override + protected FirebirdTable[] doAllTables() throws SQLException { + List tableNames = jdbcTemplate.queryForStringList( + "select RDB$RELATION_NAME as tableName\n" + + "from RDB$RELATIONS\n" + + "where RDB$VIEW_BLR is null\n" + + "and (RDB$SYSTEM_FLAG is null or RDB$SYSTEM_FLAG = 0)"); + + FirebirdTable[] tables = new FirebirdTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = getTable(tableNames.get(i)); + } + + return tables; + } + + @Override + public FirebirdTable getTable(String tableName) { + return new FirebirdTable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdTable.java new file mode 100644 index 00000000..d0a5d213 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdTable.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.firebird; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +public class FirebirdTable extends Table { + + /** + * Creates a new Firebird table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public FirebirdTable(JdbcTemplate jdbcTemplate, FirebirdDatabase database, FirebirdSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + this); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("select count(*) from RDB$RELATIONS\n" + + "where RDB$RELATION_NAME = ?\n" + + "and RDB$VIEW_BLR is null", name) > 0; + } + + @Override + protected void doLock() throws SQLException { + /* + Firebird has row-level locking on all transaction isolation levels (this requires fetching the row to lock). + Table-level locks can only be reserved in SERIALIZABLE (isc_tpb_consistency) with caveats. + This approach will read all records from table (without roundtrips to the server) to locking all records; it + will not claim a table-level lock unless the isolation level is SERIALIZABLE. This means that inserts are + still possible as are selects that don't use 'with lock'. + */ + jdbcTemplate.execute("execute block as\n" + + "declare tempvar integer;\n" + + "begin\n" + + " for select 1 from " + this + " with lock into :tempvar do\n" + + " begin\n" + + " end\n" + + "end"); + } + + @Override + public String toString() { + // No schema, only plain table name + return database.doQuote(name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/package-info.java new file mode 100644 index 00000000..97bb5b61 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.firebird; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Connection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Connection.java new file mode 100644 index 00000000..284a8759 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Connection.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.h2; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +/** + * H2 connection. + */ +public class H2Connection extends Connection { + H2Connection(H2Database database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SET SCHEMA " + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new H2Schema(jdbcTemplate, database, name); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("CALL SCHEMA()"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Database.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Database.java new file mode 100644 index 00000000..7f9ec9fc --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Database.java @@ -0,0 +1,217 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.h2; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * H2 database. + */ +public class H2Database extends Database { + + /** + * A dummy user used in Oracle mode, where USER() can return null but nulls can't be inserted into the + * schema history table + */ + private static final String DEFAULT_USER = "<< default user >>"; + /** + * A dummy script marker used in Oracle mode, where a marker row is inserted with no corresponding script. + */ + private static final String DUMMY_SCRIPT_NAME = "<< history table creation script >>"; + /** + * The compatibility modes supported by H2. See http://h2database.com/html/features.html#compatibility + */ + private enum CompatibilityMode { + REGULAR, + DB2, + Derby, + HSQLDB, + MSSQLServer, + MySQL, + Oracle, + PostgreSQL, + Ignite + } + + /** + * Whether this version supports DROP SCHEMA ... CASCADE. + */ + boolean supportsDropSchemaCascade; + + /** + * The compatibility mode of the database + */ + CompatibilityMode compatibilityMode; + + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public H2Database(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + + compatibilityMode = determineCompatibilityMode(); + } + + @Override + protected H2Connection doGetConnection(Connection connection) { + return new H2Connection(this, connection); + } + + @Override + protected MigrationVersion determineVersion() { + try { + int buildId = getMainConnection().getJdbcTemplate().queryForInt( + "SELECT VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE NAME = 'info.BUILD_ID'"); + return MigrationVersion.fromVersion(super.determineVersion().getVersion() + "." + buildId); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine H2 build ID", e); + } + } + + private CompatibilityMode determineCompatibilityMode() { + try { + String mode = getMainConnection().getJdbcTemplate().queryForString( + "SELECT VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE NAME = 'MODE'"); + if (mode == null || "".equals(mode)) + return CompatibilityMode.REGULAR; + return CompatibilityMode.valueOf(mode); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine H2 compatibility mode", e); + } + } + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("1.2.137"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("1.4", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("1.4.200"); + supportsDropSchemaCascade = getVersion().isAtLeast("1.4.200"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + // In Oracle mode, empty strings in the marker row would be converted to NULLs. As the script column is + // defined as NOT NULL, we insert a dummy value when required. + String script = (compatibilityMode == CompatibilityMode.Oracle) + ? DUMMY_SCRIPT_NAME : ""; + + return "CREATE TABLE IF NOT EXISTS " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL,\n" + + " CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\")\n" + + ")" + + // Add special table created marker to compensate for the inability of H2 to lock empty tables + " AS SELECT -1, NULL, '<< Flyway Schema History table created >>', 'TABLE', '" + script + "', NULL, '" + getInstalledBy() + "', CURRENT_TIMESTAMP, 0, TRUE;\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + @Override + public String getSelectStatement(Table table) { + return "SELECT " + quote("installed_rank") + + "," + quote("version") + + "," + quote("description") + + "," + quote("type") + + "," + quote("script") + + "," + quote("checksum") + + "," + quote("installed_on") + + "," + quote("installed_by") + + "," + quote("execution_time") + + "," + quote("success") + + " FROM " + table + // Ignore special table created marker + + " WHERE " + quote("type") + " != 'TABLE'" + + " AND " + quote("installed_rank") + " > ?" + + " ORDER BY " + quote("installed_rank"); + } + + @Override + protected String doGetCurrentUser() throws SQLException { + // In Oracle mode, empty strings in the installed_by column of the history table would be converted to NULLs. + // As H2 supports a null user, we use a dummy value when required. + String user = getMainConnection().getJdbcTemplate().queryForString("SELECT USER()"); + + if (compatibilityMode == CompatibilityMode.Oracle && (user == null || "".equals(user))) + return DEFAULT_USER; + return user; + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Parser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Parser.java new file mode 100644 index 00000000..8930db13 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Parser.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.h2; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; + +public class H2Parser extends Parser { + public H2Parser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 2); + } + + @Override + protected char getAlternativeIdentifierQuote() { + // Necessary for MySQL compatibility mode. We don't know the mode at this point so be generous and + // parse backticks even though they may not run on the database. + return '`'; + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '$'; + } + + @SuppressWarnings("Duplicates") + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + String dollarQuote = (char) reader.read() + reader.readUntilIncluding('$'); + reader.swallowUntilExcluding(dollarQuote); + reader.swallow(dollarQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Schema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Schema.java new file mode 100644 index 00000000..565d7e2d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Schema.java @@ -0,0 +1,172 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.h2; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * H2 implementation of Schema. + */ +public class H2Schema extends Schema { + private static final Log LOG = LogFactory.getLog(H2Schema.class); + + /** + * Creates a new H2 schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + H2Schema(JdbcTemplate jdbcTemplate, H2Database database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=?", name) > 0; + } + + @Override + protected boolean doEmpty() { + return allTables().length == 0; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + + (database.supportsDropSchemaCascade ? " CASCADE" : "")); + } + + @Override + protected void doClean() throws SQLException { + for (Table table : allTables()) { + table.drop(); + } + + List sequenceNames = listObjectNames("SEQUENCE", "IS_GENERATED = false"); + for (String statement : generateDropStatements("SEQUENCE", sequenceNames)) { + jdbcTemplate.execute(statement); + } + + List constantNames = listObjectNames("CONSTANT", ""); + for (String statement : generateDropStatements("CONSTANT", constantNames)) { + jdbcTemplate.execute(statement); + } + + List aliasNames = jdbcTemplate.queryForStringList( + "SELECT ALIAS_NAME FROM INFORMATION_SCHEMA.FUNCTION_ALIASES WHERE ALIAS_SCHEMA = ?", name); + for (String statement : generateDropStatements("ALIAS", aliasNames)) { + jdbcTemplate.execute(statement); + } + + List domainNames = listObjectNames("DOMAIN", ""); + if (!domainNames.isEmpty()) { + if (name.equals(database.getMainConnection().getCurrentSchema().getName())) { + for (String statement : generateDropStatementsForCurrentSchema("DOMAIN", domainNames)) { + jdbcTemplate.execute(statement); + } + } else { + LOG.error("Unable to drop DOMAIN objects in schema " + database.quote(name) + + " due to H2 bug! (More info: http://code.google.com/p/h2database/issues/detail?id=306)"); + } + } + } + + /** + * Generate the statements for dropping all the objects of this type in this schema. + * + * @param objectType The type of object to drop (Sequence, constant, ...) + * @param objectNames The names of the objects to drop. + * @return The list of statements. + */ + private List generateDropStatements(String objectType, List objectNames) { + List statements = new ArrayList<>(); + for (String objectName : objectNames) { + String dropStatement = + "DROP " + objectType + database.quote(name, objectName); + + statements.add(dropStatement); + } + return statements; + } + + /** + * Generate the statements for dropping all the objects of this type in the current schema. + * + * @param objectType The type of object to drop (Sequence, constant, ...) + * @param objectNames The names of the objects to drop. + * @return The list of statements. + */ + private List generateDropStatementsForCurrentSchema(String objectType, List objectNames) { + List statements = new ArrayList<>(); + for (String objectName : objectNames) { + String dropStatement = + "DROP " + objectType + database.quote(objectName); + + statements.add(dropStatement); + } + return statements; + } + + @Override + protected H2Table[] doAllTables() throws SQLException { + List tableNames = listObjectNames("TABLE", "TABLE_TYPE = 'TABLE'"); + + H2Table[] tables = new H2Table[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new H2Table(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + /** + * List the names of the objects of this type in this schema. + * + * @param objectType The type of objects to list (Sequence, constant, ...) + * @param querySuffix Suffix to append to the query to find the objects to list. + * @return The names of the objects. + * @throws java.sql.SQLException when the object names could not be listed. + */ + private List listObjectNames(String objectType, String querySuffix) throws SQLException { + String query = "SELECT " + objectType + "_NAME FROM INFORMATION_SCHEMA." + objectType + + "S WHERE " + objectType + "_SCHEMA = ?"; + if (StringUtils.hasLength(querySuffix)) { + query += " AND " + querySuffix; + } + + return jdbcTemplate.queryForStringList(query, name); + } + + + @Override + public Table getTable(String tableName) { + return new H2Table(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Table.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Table.java new file mode 100644 index 00000000..e96ecbb0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Table.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.h2; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * H2-specific table. + */ +public class H2Table extends Table { + /** + * Creates a new H2 table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public H2Table(JdbcTemplate jdbcTemplate, H2Database database, H2Schema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE"); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("select * from " + this + " for update"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/package-info.java new file mode 100644 index 00000000..4683370a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.h2; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBConnection.java new file mode 100644 index 00000000..1f28dd54 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBConnection.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.hsqldb; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.JdbcUtils; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * HSQLDB connection. + */ +public class HSQLDBConnection extends Connection { + HSQLDBConnection(HSQLDBDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + ResultSet resultSet = null; + String schema = null; + + try { + resultSet = database.getJdbcMetaData().getSchemas(); + while (resultSet.next()) { + if (resultSet.getBoolean("IS_DEFAULT")) { + schema = resultSet.getString("TABLE_SCHEM"); + break; + } + } + } finally { + JdbcUtils.closeResultSet(resultSet); + } + + return schema; + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SET SCHEMA " + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new HSQLDBSchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBDatabase.java new file mode 100644 index 00000000..8359ea03 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBDatabase.java @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.hsqldb; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; + +/** + * HSQLDB database. + */ +public class HSQLDBDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public HSQLDBDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected HSQLDBConnection doGetConnection(Connection connection) { + return new HSQLDBConnection(this, connection); + } + + + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("1.8"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("2.3", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("2.5"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" BIT NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");\n" + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBParser.java new file mode 100644 index 00000000..a28f2ef1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBParser.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.hsqldb; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class HSQLDBParser extends Parser { + /** + * List of objects which can be dropped with IF EXISTS + */ + private static final List CONDITIONALLY_CREATABLE_OBJECTS = Arrays.asList( + "CONSTRAINT", "TABLE", "COLUMN", "INDEX", "SEQUENCE", "VIEW", "SCHEMA" + ); + + public HSQLDBParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext,2); + } + + @Override + protected Set getValidKeywords() { + return new HashSet<>(Arrays.asList( + "ABS", "ALL", "ALLOCATE", "ALTER", "AND", "ANY", "ARE", "ARRAY", "AS", "ASENSITIVE", "ASYMMETRIC", "AT", "ATOMIC", "AUTHORIZATION", "AVG", + "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BLOB", "BOOLEAN", "BOTH", "BY", + "CALL", "CALLED", "CARDINALITY", "CASCADED", "CASE", "CAST", "CEIL", "CEILING", "CHAR", "CHAR_LENGTH", "CHARACTER", "CHARACTER_LENGTH", "CHECK", "CLOB", "CLOSE", "COALESCE", "COLLATE", "COLLECT", "COLUMN", "COMMIT", "COMPARABLE", "CONDITION", "CONNECT", "CONSTRAINT", "CONVERT", "CORR", "CORRESPONDING", "COUNT", "COVAR_POP", "COVAR_SAMP", "CREATE", "CROSS", "CUBE", "CUME_DIST", "CURRENT", "CURRENT_CATALOG", "CURRENT_DATE", "CURRENT_DEFAULT_TRANSFORM_GROUP", "CURRENT_PATH", "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_TRANSFORM_GROUP_FOR_TYPE", "CURRENT_USER", "CURSOR", "CYCLE", + "DATE", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DELETE", "DENSE_RANK", "DEREF", "DESCRIBE", "DETERMINISTIC", "DISCONNECT", "DISTINCT", "DO", "DOUBLE", "DROP", "DYNAMIC", + "EACH", "ELEMENT", "ELSE", "ELSEIF", "END", "END_EXEC", "ESCAPE", "EVERY", "EXCEPT", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXP", "EXTERNAL", "EXTRACT", + "FALSE", "FETCH", "FILTER", "FIRST_VALUE", "FLOAT", "FLOOR", "FOR", "FOREIGN", "FREE", "FROM", "FULL", "FUNCTION", "FUSION", + "GET", "GLOBAL", "GRANT", "GROUP", "GROUPING", + "HANDLER", "HAVING", "HOLD", "HOUR", + "IDENTITY", "IF", "IN", "INDEX", "INDICATOR", "INNER", "INOUT", "INSENSITIVE", "INSERT", "INT", "INTEGER", "INTERSECT", "INTERSECTION", "INTERVAL", "INTO", "IS", "ITERATE", + "JOIN", + "LAG", + "LANGUAGE", "LARGE", "LAST_VALUE", "LATERAL", "LEAD", "LEADING", "LEAVE", "LEFT", "LIKE", "LIKE_REGEX", "LN", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOOP", "LOWER", + "MATCH", "MAX", "MAX_CARDINALITY", "MEMBER", "MERGE", "METHOD", "MIN", "MINUTE", "MOD", "MODIFIES", "MODULE", "MONTH", "MULTISET", + "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NO", "NONE", "NORMALIZE", "NOT", "NTH_VALUE", "NTILE", "NULL", "NULLIF", "NUMERIC", + "OCCURRENCES_REGEX", "OCTET_LENGTH", "OF", "OFFSET", "OLD", "ON", "ONLY", "OPEN", "OR", "ORDER", "OUT", "OUTER", "OVER", "OVERLAPS", "OVERLAY", + "PARAMETER", "PARTITION", "PERCENT_RANK", "PERCENTILE_CONT", "PERCENTILE_DISC", "PERIOD", "POSITION", "POSITION_REGEX", "POWER", "PRECISION", "PREPARE", "PRIMARY", "PROCEDURE", + "RANGE", "RANK", "READS", "REAL", "RECURSIVE", "REF", "REFERENCES", "REFERENCING", "REGR_AVGX", "REGR_AVGY", "REGR_COUNT", "REGR_INTERCEPT", "REGR_R2", "REGR_SLOPE", "REGR_SXX", "REGR_SXY", "REGR_SYY", "RELEASE", "REPEAT", "RESIGNAL", "RESULT", "RETURN", "RETURNS", "REVOKE", "RIGHT", "ROLLBACK", "ROLLUP", "ROW", "ROW_NUMBER", "ROWS", + "SAVEPOINT", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SELECT", "SENSITIVE", "SEQUENCE", "SESSION_USER", "SET", "SIGNAL", "SIMILAR", "SMALLINT", "SOME", "SPECIFIC", "SPECIFICTYPE", "SQL", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "SQRT", "STACKED", "START", "STATIC", "STDDEV_POP", "STDDEV_SAMP", "SUBMULTISET", "SUBSTRING", "SUBSTRING_REGEX", "SUM", "SYMMETRIC", "SYSTEM", "SYSTEM_USER", + "TABLE", "TABLESAMPLE", "THEN", "TIME", "TIMESTAMP", "TIMEZONE_HOUR", "TIMEZONE_MINUTE", "TO", "TRAILING", "TRANSLATE", "TRANSLATE_REGEX", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRIM_ARRAY", "TRUE", "TRUNCATE", + "UESCAPE", "UNDO", "UNION", "UNIQUE", "UNKNOWN", "UNNEST", "UNTIL", "UPDATE", "UPPER", "USER", "USING", + "VALUE", "VALUES", "VAR_POP", "VAR_SAMP", "VARBINARY", "VARCHAR", "VARYING", "VIEW", + "WHEN", "WHENEVER", "WHERE", "WIDTH_BUCKET", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WHILE", + "YEAR" + )); + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + int lastKeywordIndex = getLastKeywordIndex(tokens); + Token previousKeyword = lastKeywordIndex >= 0 ? tokens.get(lastKeywordIndex) : null; + String keywordText = keyword.getText(); + String previousKeywordText = previousKeyword != null ? previousKeyword.getText() : ""; + + if ("BEGIN".equals(keywordText) + || ((("IF".equals(keywordText) && !CONDITIONALLY_CREATABLE_OBJECTS.contains(previousKeywordText)) // excludes the IF in eg. CREATE TABLE IF EXISTS + || "FOR".equals(keywordText) + || "CASE".equals(keywordText)) + && previousKeyword != null && !"END".equals(previousKeywordText))) { + context.increaseBlockDepth(); + } else if (("EACH".equals(keywordText) || "SQLEXCEPTION".equals(keywordText)) + && previousKeyword != null + && "FOR".equals(previousKeywordText)) { + context.decreaseBlockDepth(); + } else if ("END".equals(keywordText)) { + context.decreaseBlockDepth(); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBSchema.java new file mode 100644 index 00000000..cdbc556a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBSchema.java @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.hsqldb; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * HSQLDB implementation of Schema. + */ +public class HSQLDBSchema extends Schema { + /** + * Creates a new Hsql schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + HSQLDBSchema(JdbcTemplate jdbcTemplate, HSQLDBDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT (*) FROM information_schema.system_schemas WHERE table_schem=?", name) > 0; + } + + @Override + protected boolean doEmpty() { + return allTables().length == 0; + } + + @Override + protected void doCreate() throws SQLException { + String user = jdbcTemplate.queryForString("SELECT USER() FROM (VALUES(0))"); + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name) + " AUTHORIZATION " + user); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " CASCADE"); + } + + @Override + protected void doClean() throws SQLException { + for (Table table : allTables()) { + table.drop(); + } + + for (String statement : generateDropStatementsForSequences()) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generates the statements to drop the sequences in this schema. + * + * @return The drop statements. + * @throws SQLException when the drop statements could not be generated. + */ + private List generateDropStatementsForSequences() throws SQLException { + List sequenceNames = jdbcTemplate.queryForStringList( + "SELECT SEQUENCE_NAME FROM INFORMATION_SCHEMA.SYSTEM_SEQUENCES where SEQUENCE_SCHEMA = ?", name); + + List statements = new ArrayList<>(); + for (String seqName : sequenceNames) { + statements.add("DROP SEQUENCE " + database.quote(name, seqName)); + } + + return statements; + } + + @Override + protected HSQLDBTable[] doAllTables() throws SQLException { + List tableNames = jdbcTemplate.queryForStringList( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.SYSTEM_TABLES where TABLE_SCHEM = ? AND TABLE_TYPE = 'TABLE'", name); + + HSQLDBTable[] tables = new HSQLDBTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new HSQLDBTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new HSQLDBTable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBTable.java new file mode 100644 index 00000000..4fdd181c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBTable.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.hsqldb; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * HSQLDB-specific table. + */ +public class HSQLDBTable extends Table { + private static final Log LOG = LogFactory.getLog(HSQLDBTable.class); + + + + + + + + + /** + * Creates a new Hsql table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + HSQLDBTable(JdbcTemplate jdbcTemplate, HSQLDBDatabase database, HSQLDBSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + + + + + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE"); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + + + + + + + jdbcTemplate.execute("LOCK TABLE " + this + " WRITE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/package-info.java new file mode 100644 index 00000000..9625093c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.hsqldb; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixConnection.java new file mode 100644 index 00000000..8999f467 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixConnection.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.informix; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +/** + * Informix connection. + */ +public class InformixConnection extends Connection { + InformixConnection(InformixDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return getJdbcConnection().getMetaData().getUserName(); + } + + @Override + public Schema getSchema(String name) { + return new InformixSchema(jdbcTemplate, database, name); + } + + @Override + public void changeCurrentSchemaTo(Schema schema) { + // Informix doesn't support schemas + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixDatabase.java new file mode 100644 index 00000000..a5574088 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixDatabase.java @@ -0,0 +1,126 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.informix; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Informix database. + */ +public class InformixDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public InformixDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected InformixConnection doGetConnection(Connection connection) { + return new InformixConnection(this, connection); + } + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("12.10"); + recommendFlywayUpgradeIfNecessary("12.10"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = configuration.getTablespace() == null + ? "" + : " IN \"" + configuration.getTablespace() + "\""; + + return "CREATE TABLE " + table + " (\n" + + " installed_rank INT NOT NULL,\n" + + " version VARCHAR(50),\n" + + " description VARCHAR(200) NOT NULL,\n" + + " type VARCHAR(20) NOT NULL,\n" + + " script LVARCHAR(1000) NOT NULL,\n" + + " checksum INT,\n" + + " installed_by VARCHAR(100) NOT NULL,\n" + + " installed_on DATETIME YEAR TO FRACTION(3) DEFAULT CURRENT YEAR TO FRACTION(3) NOT NULL,\n" + + " execution_time INT NOT NULL,\n" + + " success SMALLINT NOT NULL\n" + + ")" + tablespace + ";\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT CHECK (success in (0,1)) CONSTRAINT " + table.getName() + "_s;\n" + + "ALTER TABLE " + table + " ADD CONSTRAINT PRIMARY KEY (installed_rank) CONSTRAINT " + table.getName() + "_pk;\n" + + "CREATE INDEX " + table.getName() + "_s_idx ON " + table + " (success);"; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getJdbcMetaData().getUserName(); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return false; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return identifier; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixParser.java new file mode 100644 index 00000000..3f3882f4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixParser.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.informix; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; + +public class InformixParser extends Parser { + public InformixParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 2); + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + int lastKeywordIndex = getLastKeywordIndex(tokens); + if (lastKeywordIndex < 0) { + return; + } + + String current = keyword.getText(); + if ("FUNCTION".equals(current) || "PROCEDURE".equals(current)) { + String previous = tokens.get(lastKeywordIndex).getText(); + + // CREATE( DBA)? (FUNCTION|PROCEDURE) + if ("CREATE".equals(previous) || "DBA".equals(previous)) { + context.increaseBlockDepth(); + } else if ("END".equals(previous)) { + context.decreaseBlockDepth(); + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixSchema.java new file mode 100644 index 00000000..2e06d7a3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixSchema.java @@ -0,0 +1,109 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.informix; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.List; + +/** + * Informix implementation of Schema. + */ +public class InformixSchema extends Schema { + /** + * Creates a new Informix schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + InformixSchema(JdbcTemplate jdbcTemplate, InformixDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM systables where owner = ? and tabid > 99", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + return doAllTables().length == 0; + } + + @Override + protected void doCreate() { + } + + @Override + protected void doDrop() throws SQLException { + clean(); + } + + @Override + protected void doClean() throws SQLException { + List procedures = jdbcTemplate.queryForStringList("SELECT t.procname FROM \"informix\".sysprocedures AS t" + + " WHERE t.owner=? AND t.mode='O' AND t.externalname IS NULL" + + " AND t.procname NOT IN (" + + // Exclude Informix TimeSeries procs + " 'tscontainerusage', 'tscontainertotalused', 'tscontainertotalpages'," + + " 'tscontainernelems', 'tscontainerpctused', 'tsl_flushstatus', 'tsmakenullstamp'" + + ")", name); + for (String procedure : procedures) { + jdbcTemplate.execute("DROP PROCEDURE " + procedure); + } + + for (Table table : allTables()) { + table.drop(); + } + + List sequences = jdbcTemplate.queryForStringList("SELECT t.tabname FROM \"informix\".systables AS t" + + " WHERE owner=? AND t.tabid > 99 AND t.tabtype='Q'" + + " AND t.tabname NOT IN ('iot_data_seq')", name); + for (String sequence : sequences) { + jdbcTemplate.execute("DROP SEQUENCE " + sequence); + } + } + + private InformixTable[] findTables(String sqlQuery, String... params) throws SQLException { + List tableNames = jdbcTemplate.queryForStringList(sqlQuery, params); + InformixTable[] tables = new InformixTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new InformixTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + protected InformixTable[] doAllTables() throws SQLException { + return findTables("SELECT t.tabname FROM \"informix\".systables AS t" + + " WHERE owner=? AND t.tabid > 99 AND t.tabtype='T'" + + " AND t.tabname NOT IN (" + + // Exclude Informix TimeSeries tables + " 'calendarpatterns', 'calendartable'," + + " 'tscontainertable', 'tscontainerwindowtable', 'tsinstancetable', " + + " 'tscontainerusageactivewindowvti', 'tscontainerusagedormantwindowvti'" + + ")", name); + } + + @Override + public Table getTable(String tableName) { + return new InformixTable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixTable.java new file mode 100644 index 00000000..8a6c7808 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixTable.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.informix; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Informix-specific table. + */ +public class InformixTable extends Table { + /** + * Creates a new Informix table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + InformixTable(JdbcTemplate jdbcTemplate, InformixDatabase database, InformixSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + name); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.update("lock table " + this + " in exclusive mode"); + } + + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/package-info.java new file mode 100644 index 00000000..25ab0a0d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.informix; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLConnection.java new file mode 100644 index 00000000..6c26345f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLConnection.java @@ -0,0 +1,163 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Callable; + +/** + * MySQL connection. + */ +public class MySQLConnection extends Connection { + private static final Log LOG = LogFactory.getLog(MySQLConnection.class); + + private static final String USER_VARIABLES_TABLE_MARIADB = "information_schema.user_variables"; + private static final String USER_VARIABLES_TABLE_MYSQL = "performance_schema.user_variables_by_thread"; + private static final String FOREIGN_KEY_CHECKS = "foreign_key_checks"; + private static final String SQL_SAFE_UPDATES = "sql_safe_updates"; + + private final String userVariablesQuery; + private final boolean canResetUserVariables; + + private final int originalForeignKeyChecks; + private final int originalSqlSafeUpdates; + + MySQLConnection(MySQLDatabase database, java.sql.Connection connection) { + super(database, connection); + + userVariablesQuery = "SELECT variable_name FROM " + + (database.isMariaDB() ? USER_VARIABLES_TABLE_MARIADB : USER_VARIABLES_TABLE_MYSQL) + + " WHERE variable_value IS NOT NULL"; + canResetUserVariables = hasUserVariableResetCapability(); + + originalForeignKeyChecks = getIntVariableValue(FOREIGN_KEY_CHECKS); + originalSqlSafeUpdates = getIntVariableValue(SQL_SAFE_UPDATES); + } + + private int getIntVariableValue(String varName) { + try { + return jdbcTemplate.queryForInt("SELECT @@" + varName); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine value for '" + varName + "' variable", e); + } + } + + // #2215: ensure the database is recent enough and the current user has the necessary SELECT grant + private boolean hasUserVariableResetCapability() { + + + + + + + + + + + + + + try { + jdbcTemplate.queryForStringList(userVariablesQuery); + return true; + } catch (SQLException e) { + LOG.debug("Disabled user variable reset as " + + (database.isMariaDB() ? USER_VARIABLES_TABLE_MARIADB : USER_VARIABLES_TABLE_MYSQL) + + "cannot be queried (SQL State: " + e.getSQLState() + ", Error Code: " + e.getErrorCode() + ")"); + return false; + } + } + + @Override + protected void doRestoreOriginalState() throws SQLException { + resetUserVariables(); + jdbcTemplate.execute("SET " + FOREIGN_KEY_CHECKS + "=?, " + SQL_SAFE_UPDATES + "=?", + originalForeignKeyChecks, originalSqlSafeUpdates); + } + + // #2197: prevent user-defined variables from leaking beyond the scope of a migration + private void resetUserVariables() throws SQLException { + if (canResetUserVariables) { + List userVariables = jdbcTemplate.queryForStringList(userVariablesQuery); + if (!userVariables.isEmpty()) { + boolean first = true; + StringBuilder setStatement = new StringBuilder("SET "); + for (String userVariable : userVariables) { + if (first) { + first = false; + } else { + setStatement.append(","); + } + setStatement.append("@").append(userVariable).append("=NULL"); + } + jdbcTemplate.executeStatement(setStatement.toString()); + } + } + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT DATABASE()"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + if (StringUtils.hasLength(schema)) { + jdbcTemplate.getConnection().setCatalog(schema); + } else { + try { + // Weird hack to switch back to no database selected... + String newDb = database.quote(UUID.randomUUID().toString()); + jdbcTemplate.execute("CREATE SCHEMA " + newDb); + jdbcTemplate.execute("USE " + newDb); + jdbcTemplate.execute("DROP SCHEMA " + newDb); + } catch (Exception e) { + LOG.warn("Unable to restore connection to having no default schema: " + e.getMessage()); + } + } + } + + @Override + protected Schema doGetCurrentSchema() throws SQLException { + String schemaName = getCurrentSchemaNameOrSearchPath(); + + // #2206: MySQL and MariaDB can have URLs where no current schema is set, so we must handle this case explicitly. + return schemaName == null ? null : getSchema(schemaName); + } + + @Override + public Schema getSchema(String name) { + return new MySQLSchema(jdbcTemplate, database, name); + } + + @Override + public T lock(Table table, Callable callable) { + if (database.isPxcStrict()) { + return super.lock(table, callable); + } + return new MySQLNamedLockTemplate(jdbcTemplate, table.toString().hashCode()).execute(callable); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java new file mode 100644 index 00000000..f408cd99 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java @@ -0,0 +1,348 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.JdbcUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MySQL database. + */ +public class MySQLDatabase extends Database { + // See https://mariadb.com/kb/en/version/ + private static final Pattern MARIADB_VERSION_PATTERN = Pattern.compile("(\\d+\\.\\d+)\\.\\d+(-\\d+)*-MariaDB(-\\w+)*"); + private static final Pattern MARIADB_WITH_MAXSCALE_VERSION_PATTERN = Pattern.compile("(\\d+\\.\\d+)\\.\\d+(-\\d+)* (\\d+\\.\\d+)\\.\\d+(-\\d+)*-maxscale(-\\w+)*"); + private static final Pattern MYSQL_VERSION_PATTERN = Pattern.compile("(\\d+\\.\\d+)\\.\\d+\\w*"); + private static final Log LOG = LogFactory.getLog(MySQLDatabase.class); + + /** + * Whether this is a Percona XtraDB Cluster in strict mode. + */ + private final boolean pxcStrict; + + /** + * Whether this database is enforcing GTID consistency. + */ + private final boolean gtidConsistencyEnforced; + + /** + * Whether the event scheduler table is queryable. + */ + final boolean eventSchedulerQueryable; + + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public MySQLDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(rawMainJdbcConnection, databaseType); + pxcStrict = isMySQL() && isRunningInPerconaXtraDBClusterWithStrictMode(jdbcTemplate); + gtidConsistencyEnforced = isMySQL() && isRunningInGTIDConsistencyMode(jdbcTemplate); + eventSchedulerQueryable = isMySQL() || isEventSchedulerQueryable(jdbcTemplate); + } + + private static boolean isEventSchedulerQueryable(JdbcTemplate jdbcTemplate) { + try { + // Attempt query + jdbcTemplate.queryForString("SELECT event_name FROM information_schema.events LIMIT 1"); + return true; + } catch (SQLException e) { + LOG.debug("Detected unqueryable MariaDB event scheduler, most likely due to it being OFF or DISABLED."); + return false; + } + } + + static boolean isRunningInPerconaXtraDBClusterWithStrictMode(JdbcTemplate jdbcTemplate) { + try { + if ("ENFORCING".equals(jdbcTemplate.queryForString( + "select VARIABLE_VALUE from performance_schema.global_variables" + + " where variable_name = 'pxc_strict_mode'"))) { + LOG.debug("Detected Percona XtraDB Cluster in strict mode"); + return true; + } + } catch (SQLException e) { + LOG.debug("Unable to detect whether we are running in a Percona XtraDB Cluster. Assuming not to be."); + } + + return false; + } + + static boolean isRunningInGTIDConsistencyMode(JdbcTemplate jdbcTemplate) { + try { + String gtidConsistency = jdbcTemplate.queryForString("SELECT @@GLOBAL.ENFORCE_GTID_CONSISTENCY"); + if ("ON".equals(gtidConsistency)) { + LOG.debug("Detected GTID consistency being enforced"); + return true; + } + } catch (SQLException e) { + LOG.debug("Unable to detect whether database enforces GTID consistency. Assuming not."); + } + + return false; + } + + boolean isMySQL() { + return databaseType == DatabaseType.MYSQL; + } + + boolean isMariaDB() { + return databaseType == DatabaseType.MARIADB; + } + + boolean isPxcStrict() { + return pxcStrict; + } + + /* + * CREATE TABLE ... AS SELECT ... cannot be used in two scenarios: + * - Percona XtraDB Cluster in strict mode doesn't support it + * - When GTID consistency is being enforced. Note that if GTID_MODE is ON, then ENFORCE_GTID_CONSISTENCY is + * necessarily ON as well. + */ + private boolean isCreateTableAsSelectAllowed() { + return !pxcStrict && !gtidConsistencyEnforced; + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = + + + + configuration.getTablespace() == null + ? "" + : " TABLESPACE \"" + configuration.getTablespace() + "\""; + + String baselineMarker = ""; + if (baseline) { + if (isCreateTableAsSelectAllowed()) { + baselineMarker = " AS SELECT" + + " 1 as \"installed_rank\"," + + " '" + configuration.getBaselineVersion() + "' as \"version\"," + + " '" + configuration.getBaselineDescription() + "' as \"description\"," + + " '" + MigrationType.BASELINE + "' as \"type\"," + + " '" + configuration.getBaselineDescription() + "' as \"script\"," + + " NULL as \"checksum\"," + + " '" + getInstalledBy() + "' as \"installed_by\"," + + " CURRENT_TIMESTAMP as \"installed_on\"," + + " 0 as \"execution_time\"," + + " TRUE as \"success\"\n"; + } else { + // Revert to regular insert, which unfortunately is not safe in concurrent scenarios + // due to MySQL implicit commits after DDL statements. + baselineMarker = ";\n" + getBaselineStatement(table); + } + } + + return "CREATE TABLE " + table + " (\n" + + " `installed_rank` INT NOT NULL,\n" + + " `version` VARCHAR(50),\n" + + " `description` VARCHAR(200) NOT NULL,\n" + + " `type` VARCHAR(20) NOT NULL,\n" + + " `script` VARCHAR(1000) NOT NULL,\n" + + " `checksum` INT,\n" + + " `installed_by` VARCHAR(100) NOT NULL,\n" + + " `installed_on` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " `execution_time` INT NOT NULL,\n" + + " `success` BOOL NOT NULL,\n" + + " CONSTRAINT `" + table.getName() + "_pk` PRIMARY KEY (`installed_rank`)\n" + + ")" + tablespace + " ENGINE=InnoDB" + + baselineMarker + + ";\n" + + "CREATE INDEX `" + table.getName() + "_s_idx` ON " + table + " (`success`);"; + } + + @Override + protected MySQLConnection doGetConnection(Connection connection) { + return new MySQLConnection(this, connection); + } + + @Override + protected MigrationVersion determineVersion() { + String selectVersionOutput = DatabaseType.getSelectVersionOutput(rawMainJdbcConnection); + if (databaseType == DatabaseType.MARIADB) { + try { + String productVersion = jdbcMetaData.getDatabaseProductVersion(); + return correctForAzureMariaDB(productVersion, selectVersionOutput); + + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine MariaDB server version", e); + } + } + MigrationVersion jdbcMetadataVersion = super.determineVersion(); + return correctForMySQLWithBadMetadata(jdbcMetadataVersion, selectVersionOutput); + } + + /* + * Azure Database for MySQL reports version numbers incorrectly - it claims to be 5.6 (the gateway + * version) while the db itself is 5.7 or greater, visible from SELECT VERSION(). We work around this specific + * case. This code should be simplified as soon as Azure is fixed. + * https://docs.microsoft.com/en-us/azure/mysql/concepts-limits#current-known-issues + * A similar issue applies to Percona, except there the metadata claims to be 5.5. + */ + static MigrationVersion correctForMySQLWithBadMetadata(MigrationVersion jdbcMetadataVersion, String selectVersionOutput) { + if (selectVersionOutput.compareTo("5.7") >= 0 && jdbcMetadataVersion.toString().compareTo("5.7") < 0) { + LOG.debug("MySQL-based database - reporting v" + jdbcMetadataVersion.toString() +" in JDBC metadata but database actually v" + selectVersionOutput); + return extractVersionFromString(selectVersionOutput, MYSQL_VERSION_PATTERN); + } + return jdbcMetadataVersion; + } + + /* + * Azure Database for MariaDB also reports version numbers incorrectly - it claims to be MySQL 5.6 (the gateway + * version) while the db itself is something like 10.3.6-MariaDB-suffix, visible from SELECT VERSION(). + * This code should be simplified as soon as Azure is fixed. + * https://docs.microsoft.com/en-us/azure/mysql/concepts-limits#current-known-issues + * https://mariadb.com/kb/en/server-system-variables/#version + */ + static MigrationVersion correctForAzureMariaDB(String jdbcMetadataVersion, String selectVersionOutput) { + if (jdbcMetadataVersion.startsWith("5.6")) { + LOG.debug("Azure MariaDB database - reporting v5.6 in JDBC metadata but database actually v" + selectVersionOutput); + return extractVersionFromString(selectVersionOutput, MARIADB_VERSION_PATTERN, MARIADB_WITH_MAXSCALE_VERSION_PATTERN); + } + return extractVersionFromString(jdbcMetadataVersion, MARIADB_VERSION_PATTERN, MARIADB_WITH_MAXSCALE_VERSION_PATTERN); + } + + /* + * Given a version string that may contain unwanted text, extract out the version part. + */ + private static MigrationVersion extractVersionFromString(String versionString, Pattern... patterns) { + for (Pattern pattern : patterns) { + Matcher matcher = pattern.matcher(versionString); + if (matcher.find()) { + return MigrationVersion.fromVersion(matcher.group(1)); + } + } + throw new FlywayException("Unable to determine version from '" + versionString + "'"); + } + + + + + + + + + + + + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("5.1"); + if (databaseType == DatabaseType.MARIADB) { + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("10.1", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("10.2", org.flywaydb.core.internal.license.Edition.PRO); + + recommendFlywayUpgradeIfNecessary("10.4"); + } else { + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("5.7", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + + + + if (JdbcUtils.getDriverName(jdbcMetaData).contains("MariaDB")) { + LOG.warn("You are connected to a MySQL " + getVersion() + " database using the MariaDB driver." + + " This is known to cause issues." + + " An upgrade to Oracle's MySQL JDBC driver is highly recommended."); + } + + + + recommendFlywayUpgradeIfNecessary("8.0"); + } + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT SUBSTRING_INDEX(USER(),'@',1)"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "`" + identifier + "`"; + } + + @Override + public boolean catalogIsSchema() { + return true; + } + + @Override + public boolean useSingleConnection() { + return !pxcStrict; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLNamedLockTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLNamedLockTemplate.java new file mode 100644 index 00000000..ccb3930c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLNamedLockTemplate.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing with MySQL named locks. + */ +public class MySQLNamedLockTemplate { + private static final Log LOG = LogFactory.getLog(MySQLNamedLockTemplate.class); + + /** + * The connection for the named lock. + */ + private final JdbcTemplate jdbcTemplate; + + private final String lockName; + + /** + * Creates a new named lock template for this connection. + * + * @param jdbcTemplate The jdbcTemplate for the connection. + * @param discriminator A number to discriminate between locks. + */ + MySQLNamedLockTemplate(JdbcTemplate jdbcTemplate, int discriminator) { + this.jdbcTemplate = jdbcTemplate; + lockName = "Flyway-" + discriminator; + } + + /** + * Executes this callback with a named lock. + * + * @param callable The callback to execute. + * @return The result of the callable code. + */ + public T execute(Callable callable) { + try { + lock(); + return callable.call(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to acquire MySQL named lock: " + lockName, e); + } catch (Exception e) { + RuntimeException rethrow; + if (e instanceof RuntimeException) { + rethrow = (RuntimeException) e; + } else { + rethrow = new FlywayException(e); + } + throw rethrow; + } finally { + try { + jdbcTemplate.execute("SELECT RELEASE_LOCK('" + lockName + "')"); + } catch (SQLException e) { + LOG.error("Unable to release MySQL named lock: " + lockName, e); + } + } + } + + private void lock() throws SQLException { + while (!tryLock()) { + try { + Thread.sleep(100L); + } catch (InterruptedException e) { + throw new FlywayException("Interrupted while attempting to acquire MySQL named lock: " + lockName, e); + } + } + } + + private boolean tryLock() throws SQLException { + return jdbcTemplate.queryForInt("SELECT GET_LOCK(?,10)", lockName) == 1; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.java new file mode 100644 index 00000000..45b13e99 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.java @@ -0,0 +1,210 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class MySQLParser extends Parser { + private static final char ALTERNATIVE_SINGLE_LINE_COMMENT = '#'; + private IfState ifState; + private String previousKeywordText; + + enum IfState { + NONE, + IF_FUNCTION, + IF_NOT, + IF_EXISTS, + IF, + IF_THEN, + UNKNOWN + } + + public MySQLParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 8); + ifState = IfState.NONE; + previousKeywordText = ""; + } + + @Override + protected void resetDelimiter(ParserContext context) { + // Do not reset delimiter as delimiter changes survive beyond a single statement + } + + @Override + protected Token handleKeyword(PeekingReader reader, ParserContext context, int pos, int line, int col, String keyword) throws IOException { + if (keywordIs("DELIMITER", keyword)) { + String text = reader.readUntilExcluding('\n', '\r').trim(); + return new Token(TokenType.NEW_DELIMITER, pos, line, col, text, text, context.getParensDepth()); + } + return super.handleKeyword(reader, context, pos, line, col, keyword); + } + + @Override + protected char getIdentifierQuote() { + return '`'; + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '"'; + } + + @Override + protected boolean isSingleLineComment(String peek, ParserContext context, int col) { + return (super.isSingleLineComment(peek, context, col) + // Normally MySQL treats # as a comment, but this may have been overridden by DELIMITER # directive + || (peek.charAt(0) == ALTERNATIVE_SINGLE_LINE_COMMENT && !isDelimiter(peek, context, col))); + } + + @Override + protected Token handleStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(); + reader.swallowUntilExcludingWithEscape('\'', true, '\\'); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(); + reader.swallowUntilExcludingWithEscape('"', true, '\\'); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + @Override + protected Token handleCommentDirective(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + String text = reader.readUntilExcluding("*/"); + reader.swallow(2); + return new Token(TokenType.MULTI_LINE_COMMENT_DIRECTIVE, pos, line, col, text, text, context.getParensDepth()); + } + + @Override + protected boolean isCommentDirective(String text) { + return text.length() >= 8 + && text.charAt(0) == '/' + && text.charAt(1) == '*' + && text.charAt(2) == '!' + && isDigit(text.charAt(3)) + && isDigit(text.charAt(4)) + && isDigit(text.charAt(5)) + && isDigit(text.charAt(6)) + && isDigit(text.charAt(7)); + } + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, Token token) { + TokenType tokenType = token.getType(); + if (TokenType.DELIMITER == tokenType || ";".equals(token.getText())) { + return true; + } + + return super.shouldAdjustBlockDepth(context, token); + } + + // These words increase the block depth - unless preceded by END (in which case the END will decrease the block depth) + // See: https://dev.mysql.com/doc/refman/8.0/en/flow-control-statements.html + private static final List CONTROL_FLOW_KEYWORDS = Arrays.asList("LOOP", "CASE", "REPEAT", "WHILE"); + + private static final Pattern CREATE_IF_NOT_EXISTS = Pattern.compile( + ".*CREATE\\s([^\\s]+\\s){1,2}IF\\sNOT\\sEXISTS"); + private static final Pattern DROP_IF_EXISTS = Pattern.compile( + ".*DROP\\s([^\\s]+\\s){1,2}IF\\sEXISTS"); + + private boolean doesDelimiterEndFunction(List tokens, Token delimiter) { + + // if there's not enough tokens, its not the function + if (tokens.size() < 2) { + return false; + } + + // if the previous keyword was not inside some brackets, it's not the function + if (tokens.get(tokens.size()-1).getParensDepth() != delimiter.getParensDepth()+1) { + return false; + } + + // if the previous token was not IF or REPEAT, it's not the function + Token previousToken = getPreviousToken(tokens, delimiter.getParensDepth()); + if (previousToken == null || !("IF".equals(previousToken.getText()) || "REPEAT".equals(previousToken.getText()))) { + return false; + } + + return true; + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + String keywordText = keyword.getText(); + + int parensDepth = keyword.getParensDepth(); + + if (IfState.IF.equals(ifState)) { + ifState = IfState.UNKNOWN; + + if (keywordText.equals("EXISTS")) { + ifState = IfState.IF_EXISTS; + } + + if (keywordText.equals("NOT")) { + ifState = IfState.IF_NOT; + } + } + + if (keywordText.equals("THEN")) { + ifState = IfState.IF_THEN; + } + + if (keywordText.equals("IF") && !previousKeywordText.equals("END") && !IfState.IF_FUNCTION.equals(ifState)) { + if (IfState.IF_EXISTS.equals(ifState) || IfState.IF_NOT.equals(ifState)) { + context.decreaseBlockDepth(); + } + + context.increaseBlockDepth(); + if (reader.peekNextNonWhitespace() == '(') { + ifState = IfState.IF_FUNCTION; + } else { + ifState = IfState.IF; + } + } + + if ("BEGIN".equals(keywordText) || (CONTROL_FLOW_KEYWORDS.contains(keywordText) && !lastTokenIs(tokens, parensDepth, "END"))) { + context.increaseBlockDepth(); + } + + if ("END".equals(keywordText)) { + context.decreaseBlockDepth(); + if (IfState.IF_THEN.equals(ifState)) { + ifState = IfState.NONE; + } + } + + if (";".equals(keywordText) || TokenType.DELIMITER.equals(keyword.getType())) { + if (IfState.IF_NOT.equals(ifState) || IfState.IF_EXISTS.equals(ifState) || IfState.IF_FUNCTION.equals(ifState)) { + context.decreaseBlockDepth(); + ifState = IfState.NONE; + } else if (context.getBlockDepth() > 0 && doesDelimiterEndFunction(tokens, keyword)) { + context.decreaseBlockDepth(); + } + } + + previousKeywordText = keywordText; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLSchema.java new file mode 100644 index 00000000..c50408cc --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLSchema.java @@ -0,0 +1,201 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * MySQL implementation of Schema. + */ +public class MySQLSchema extends Schema { + /** + * Creates a new MySQL schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + MySQLSchema(JdbcTemplate jdbcTemplate, MySQLDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT (SELECT 1 FROM information_schema.schemata WHERE schema_name=? LIMIT 1)", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + List params = new ArrayList<>(Arrays.asList(name, name, name, name, name)); + if (database.eventSchedulerQueryable) { + params.add(name); + } + + return jdbcTemplate.queryForInt("SELECT SUM(found) FROM (" + + "(SELECT 1 as found FROM information_schema.tables WHERE table_schema=?) UNION ALL " + + "(SELECT 1 as found FROM information_schema.views WHERE table_schema=? LIMIT 1) UNION ALL " + + "(SELECT 1 as found FROM information_schema.table_constraints WHERE table_schema=? LIMIT 1) UNION ALL " + + "(SELECT 1 as found FROM information_schema.triggers WHERE event_object_schema=? LIMIT 1) UNION ALL " + + "(SELECT 1 as found FROM information_schema.routines WHERE routine_schema=? LIMIT 1)" + // #2410 Unlike MySQL, MariaDB 10.0 and newer don't allow the events table to be queried + // when the event scheduled is DISABLED or in some rare cases OFF + + (database.eventSchedulerQueryable ? " UNION ALL (SELECT 1 as found FROM information_schema.events WHERE event_schema=? LIMIT 1)" : "") + + ") as all_found", + params.toArray(new String[0]) + ) == 0; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name)); + } + + @Override + protected void doClean() throws SQLException { + if (database.eventSchedulerQueryable) { + for (String statement : cleanEvents()) { + jdbcTemplate.execute(statement); + } + } + + for (String statement : cleanRoutines()) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanViews()) { + jdbcTemplate.execute(statement); + } + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); + for (Table table : allTables()) { + table.drop(); + } + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1"); + + // MariaDB 10.3 and newer only + for (String statement : cleanSequences()) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generate the statements to clean the events in this schema. + * + * @return The list of statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanEvents() throws SQLException { + List eventNames = + jdbcTemplate.queryForStringList( + "SELECT event_name FROM information_schema.events WHERE event_schema=?", + name); + + List statements = new ArrayList<>(); + for (String eventName : eventNames) { + statements.add("DROP EVENT " + database.quote(name, eventName)); + } + return statements; + } + + /** + * Generate the statements to clean the routines in this schema. + * + * @return The list of statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanRoutines() throws SQLException { + List> routineNames = + jdbcTemplate.queryForList( + "SELECT routine_name as 'N', routine_type as 'T' FROM information_schema.routines WHERE routine_schema=?", + name); + + List statements = new ArrayList<>(); + for (Map row : routineNames) { + String routineName = row.get("N"); + String routineType = row.get("T"); + statements.add("DROP " + routineType + " " + database.quote(name, routineName)); + } + return statements; + } + + /** + * Generate the statements to clean the views in this schema. + * + * @return The list of statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanViews() throws SQLException { + List viewNames = + jdbcTemplate.queryForStringList( + "SELECT table_name FROM information_schema.views WHERE table_schema=?", name); + + List statements = new ArrayList<>(); + for (String viewName : viewNames) { + statements.add("DROP VIEW " + database.quote(name, viewName)); + } + return statements; + } + + /** + * Generate the statements to clean the sequences in this schema. + * + * @return The list of statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanSequences() throws SQLException { + List names = + jdbcTemplate.queryForStringList( + "SELECT table_name FROM information_schema.tables WHERE table_schema=?" + + " AND table_type='SEQUENCE'", name); + + List statements = new ArrayList<>(); + for (String name : names) { + statements.add("DROP SEQUENCE " + database.quote(this.name, name)); + } + return statements; + } + + @Override + protected MySQLTable[] doAllTables() throws SQLException { + List tableNames = jdbcTemplate.queryForStringList( + "SELECT table_name FROM information_schema.tables WHERE table_schema=?" + + " AND table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')", name); + + MySQLTable[] tables = new MySQLTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new MySQLTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new MySQLTable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLTable.java new file mode 100644 index 00000000..b9e982da --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLTable.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.mysql; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * MySQL-specific table. + */ +public class MySQLTable extends Table { + /** + * Creates a new MySQL table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + MySQLTable(JdbcTemplate jdbcTemplate, MySQLDatabase database, MySQLSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name)); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(schema, null, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("SELECT * FROM " + this + " FOR UPDATE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/package-info.java new file mode 100644 index 00000000..a897d1d0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.mysql; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleConnection.java new file mode 100644 index 00000000..ad75b676 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleConnection.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +/** + * Oracle connection. + */ +public class OracleConnection extends Connection { + OracleConnection(OracleDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("ALTER SESSION SET CURRENT_SCHEMA=" + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new OracleSchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleDatabase.java new file mode 100644 index 00000000..f05b62de --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleDatabase.java @@ -0,0 +1,357 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.RowMapper; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Oracle database. + */ +public class OracleDatabase extends Database { + private static final String ORACLE_NET_TNS_ADMIN = "oracle.net.tns_admin"; + + /** + * If the TNS_ADMIN environment variable is set, enable tnsnames.ora support for the Oracle JDBC driver. + * See http://www.orafaq.com/wiki/TNS_ADMIN + */ + public static void enableTnsnamesOraSupport() { + String tnsAdminEnvVar = System.getenv("TNS_ADMIN"); + String tnsAdminSysProp = System.getProperty(ORACLE_NET_TNS_ADMIN); + if (StringUtils.hasLength(tnsAdminEnvVar) && tnsAdminSysProp == null) { + System.setProperty(ORACLE_NET_TNS_ADMIN, tnsAdminEnvVar); + } + } + + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public OracleDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected OracleConnection doGetConnection(Connection connection) { + return new OracleConnection(this, connection); + } + + + + + + + + + + + + + + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("10"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("12.2", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("19.0"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = configuration.getTablespace() == null + ? "" + : " TABLESPACE \"" + configuration.getTablespace() + "\""; + + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR2(50),\n" + + " \"description\" VARCHAR2(200) NOT NULL,\n" + + " \"type\" VARCHAR2(20) NOT NULL,\n" + + " \"script\" VARCHAR2(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR2(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" NUMBER(1) NOT NULL,\n" + + " CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\")\n" + + ")" + tablespace + ";\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");\n"; + } + + @Override + public boolean supportsEmptyMigrationDescription() { + // Oracle will convert the empty string to NULL implicitly, and throw an exception as the column is NOT NULL + return false; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT USER FROM DUAL"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + /** + * Checks whether the specified query returns rows or not. Wraps the query in EXISTS() SQL function and executes it. + * This is more preferable to opening a cursor for the original query, because a query inside EXISTS() is implicitly + * optimized to return the first row and because the client never fetches more than 1 row despite the fetch size + * value. + * + * @param query The query to check. + * @param params The query parameters. + * @return {@code true} if the query returns rows, {@code false} if not. + * @throws SQLException when the query execution failed. + */ + boolean queryReturnsRows(String query, String... params) throws SQLException { + return getMainConnection().getJdbcTemplate().queryForBoolean("SELECT CASE WHEN EXISTS(" + query + ") THEN 1 ELSE 0 END FROM DUAL", params); + } + + /** + * Checks whether the specified privilege or role is granted to the current user. + * + * @return {@code true} if it is granted, {@code false} if not. + * @throws SQLException if the check failed. + */ + boolean isPrivOrRoleGranted(String name) throws SQLException { + return queryReturnsRows("SELECT 1 FROM SESSION_PRIVS WHERE PRIVILEGE = ? UNION ALL " + + "SELECT 1 FROM SESSION_ROLES WHERE ROLE = ?", name, name); + } + + /** + * Checks whether the specified data dictionary view in the specified system schema is accessible (directly or + * through a role) or not. + * + * @param owner the schema name, unquoted case-sensitive. + * @param name the data dictionary view name to check, unquoted case-sensitive. + * @return {@code true} if it is accessible, {@code false} if not. + * @throws SQLException if the check failed. + */ + private boolean isDataDictViewAccessible(String owner, String name) throws SQLException { + return queryReturnsRows("SELECT * FROM ALL_TAB_PRIVS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?" + + " AND PRIVILEGE = 'SELECT'", owner, name); + } + + /** + * Checks whether the specified SYS view is accessible (directly or through a role) or not. + * + * @param name the data dictionary view name to check, unquoted case-sensitive. + * @return {@code true} if it is accessible, {@code false} if not. + * @throws SQLException if the check failed. + */ + boolean isDataDictViewAccessible(String name) throws SQLException { + return isDataDictViewAccessible("SYS", name); + } + + /** + * Returns the specified data dictionary view name prefixed with DBA_ or ALL_ depending on its accessibility. + * + * @param baseName the data dictionary view base name, unquoted case-sensitive, e.g. OBJECTS, TABLES. + * @return the full name of the view with the proper prefix. + * @throws SQLException if the check failed. + */ + String dbaOrAll(String baseName) throws SQLException { + return isPrivOrRoleGranted("SELECT ANY DICTIONARY") || isDataDictViewAccessible("DBA_" + baseName) + ? "DBA_" + baseName + : "ALL_" + baseName; + } + + /** + * Returns the set of Oracle options available on the target database. + * + * @return the set of option titles. + * @throws SQLException if retrieving of options failed. + */ + private Set getAvailableOptions() throws SQLException { + return new HashSet<>(getMainConnection().getJdbcTemplate() + .queryForStringList("SELECT PARAMETER FROM V$OPTION WHERE VALUE = 'TRUE'")); + } + + /** + * Checks whether Flashback Data Archive option is available or not. + * + * @return {@code true} if it is available, {@code false} if not. + * @throws SQLException when checking availability of the feature failed. + */ + boolean isFlashbackDataArchiveAvailable() throws SQLException { + return getAvailableOptions().contains("Flashback Data Archive"); + } + + /** + * Checks whether XDB component is available or not. + * + * @return {@code true} if it is available, {@code false} if not. + * @throws SQLException when checking availability of the component failed. + */ + boolean isXmlDbAvailable() throws SQLException { + return isDataDictViewAccessible("ALL_XML_TABLES"); + } + + /** + * Checks whether Data Mining option is available or not. + * + * @return {@code true} if it is available, {@code false} if not. + * @throws SQLException when checking availability of the feature failed. + */ + boolean isDataMiningAvailable() throws SQLException { + return getAvailableOptions().contains("Data Mining"); + } + + /** + * Checks whether Oracle Locator component is available or not. + * + * @return {@code true} if it is available, {@code false} if not. + * @throws SQLException when checking availability of the component failed. + */ + boolean isLocatorAvailable() throws SQLException { + return isDataDictViewAccessible("MDSYS", "ALL_SDO_GEOM_METADATA"); + } + + /** + * Returns the list of schemas that were created and are maintained by Oracle-supplied scripts and must not be + * changed in any other way. The list is composed of default schemas mentioned in the official documentation for + * Oracle Database versions from 10.1 to 12.2, and is dynamically extended with schemas from DBA_REGISTRY and + * ALL_USERS (marked with ORACLE_MAINTAINED = 'Y' in Oracle 12c). + * + * @return the set of system schema names + */ + Set getSystemSchemas() throws SQLException { + + // The list of known default system schemas + Set result = new HashSet<>(Arrays.asList( + "SYS", "SYSTEM", // Standard system accounts + "SYSBACKUP", "SYSDG", "SYSKM", "SYSRAC", "SYS$UMF", // Auxiliary system accounts + "DBSNMP", "MGMT_VIEW", "SYSMAN", // Enterprise Manager accounts + "OUTLN", // Stored outlines + "AUDSYS", // Unified auditing + "ORACLE_OCM", // Oracle Configuration Manager + "APPQOSSYS", // Oracle Database QoS Management + "OJVMSYS", // Oracle JavaVM + "DVF", "DVSYS", // Oracle Database Vault + "DBSFWUSER", // Database Service Firewall + "REMOTE_SCHEDULER_AGENT", // Remote scheduler agent + "DIP", // Oracle Directory Integration Platform + "APEX_PUBLIC_USER", "FLOWS_FILES", /*"APEX_######", "FLOWS_######",*/ // Oracle Application Express + "ANONYMOUS", "XDB", "XS$NULL", // Oracle XML Database + "CTXSYS", // Oracle Text + "LBACSYS", // Oracle Label Security + "EXFSYS", // Oracle Rules Manager and Expression Filter + "MDDATA", "MDSYS", "SPATIAL_CSW_ADMIN_USR", "SPATIAL_WFS_ADMIN_USR", // Oracle Locator and Spatial + "ORDDATA", "ORDPLUGINS", "ORDSYS", "SI_INFORMTN_SCHEMA", // Oracle Multimedia + "WMSYS", // Oracle Workspace Manager + "OLAPSYS", // Oracle OLAP catalogs + "OWBSYS", "OWBSYS_AUDIT", // Oracle Warehouse Builder + "GSMADMIN_INTERNAL", "GSMCATUSER", "GSMUSER", // Global Data Services + "GGSYS", // Oracle GoldenGate + "WK_TEST", "WKSYS", "WKPROXY", // Oracle Ultra Search + "ODM", "ODM_MTR", "DMSYS", // Oracle Data Mining + "TSMSYS" // Transparent Session Migration + )); + + + + + + + result.addAll(getMainConnection().getJdbcTemplate().queryForStringList("SELECT USERNAME FROM ALL_USERS " + + "WHERE REGEXP_LIKE(USERNAME, '^(APEX|FLOWS)_\\d+$')" + + + + + " OR ORACLE_MAINTAINED = 'Y'" + + + + )); + + + + + + + + + + + + + + + + + + + + + + + + + return result; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java new file mode 100644 index 00000000..fc3a7826 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java @@ -0,0 +1,536 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.api.configuration.Configuration; + +import org.flywaydb.core.internal.parser.*; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class OracleParser extends Parser { + + + + + + + + + + + + + + /** + * Delimiter of PL/SQL blocks and statements. + */ + private static final Delimiter PLSQL_DELIMITER = new Delimiter("/", true + + + + ); + + + + + + private static final Pattern PLSQL_TYPE_BODY_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sTYPE\\sBODY\\s([^\\s]*\\s)?(IS|AS)"); + + private static final Pattern PLSQL_PACKAGE_BODY_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE\\sBODY\\s([^\\s]*\\s)?(IS|AS)"); + private static final StatementType PLSQL_PACKAGE_BODY_STATEMENT = new StatementType(); + + private static final Pattern PLSQL_PACKAGE_DEFINITION_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE\\s([^\\s]*\\s)?(AUTHID\\s[^\\s]*\\s)?(IS|AS)"); + + private static final Pattern PLSQL_VIEW_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sVIEW\\s([^\\s]*\\s)?AS\\sWITH\\s(PROCEDURE|FUNCTION)"); + private static final StatementType PLSQL_VIEW_STATEMENT = new StatementType(); + + private static final Pattern PLSQL_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\s(FUNCTION|PROCEDURE|TYPE|TRIGGER)"); + private static final Pattern DECLARE_BEGIN_REGEX = Pattern.compile("^DECLARE|BEGIN|WITH"); + private static final StatementType PLSQL_STATEMENT = new StatementType(); + + private static final Pattern JAVA_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?(\\sAND\\s(RESOLVE|COMPILE))?(\\sNOFORCE)?\\sJAVA\\s(SOURCE|RESOURCE|CLASS)"); + private static final StatementType PLSQL_JAVA_STATEMENT = new StatementType(); + + private static Pattern toRegex(String... commands) { + return Pattern.compile(toRegexPattern(commands)); + } + + private static String toRegexPattern(String... commands) { + return "^(" + StringUtils.arrayToDelimitedString("|", commands) + ")"; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public OracleParser(Configuration configuration + + + + + + + , ParsingContext parsingContext + ) { + super(configuration, parsingContext, 3); + + + + + + + } + + + + + + + + + + + + + + + + + + + @Override + protected ParsedSqlStatement createStatement(PeekingReader reader, Recorder recorder, + int statementPos, int statementLine, int statementCol, + int nonCommentPartPos, int nonCommentPartLine, int nonCommentPartCol, + StatementType statementType, boolean canExecuteInTransaction, + Delimiter delimiter, String sql + + + + ) throws IOException { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if (PLSQL_VIEW_STATEMENT == statementType) { + sql = sql.trim(); + + // Strip extra semicolon to avoid issues with WITH statements containing PL/SQL + if (sql.endsWith(";")) { + sql = sql.substring(0, sql.length() - 1); + } + } + + return super.createStatement(reader, recorder, statementPos, statementLine, statementCol, + nonCommentPartPos, nonCommentPartLine, nonCommentPartCol, + statementType, canExecuteInTransaction, delimiter, sql + + + + ); + } + + @Override + protected StatementType detectStatementType(String simplifiedStatement) { + if (PLSQL_PACKAGE_BODY_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_PACKAGE_BODY_STATEMENT; + } + + if (PLSQL_REGEX.matcher(simplifiedStatement).matches() + || PLSQL_PACKAGE_DEFINITION_REGEX.matcher(simplifiedStatement).matches() + || DECLARE_BEGIN_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_STATEMENT; + } + + if (JAVA_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_JAVA_STATEMENT; + } + + if (PLSQL_VIEW_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_VIEW_STATEMENT; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + return super.detectStatementType(simplifiedStatement); + } + + @Override + protected boolean shouldDiscard(Token token, boolean nonCommentPartSeen) { + // Discard dangling PL/SQL / delimiters + return ("/".equals(token.getText()) && !nonCommentPartSeen) || super.shouldDiscard(token, nonCommentPartSeen); + } + + @Override + protected void adjustDelimiter(ParserContext context, StatementType statementType) { + if (statementType == PLSQL_STATEMENT || statementType == PLSQL_VIEW_STATEMENT || statementType == PLSQL_JAVA_STATEMENT + || statementType == PLSQL_PACKAGE_BODY_STATEMENT) { + context.setDelimiter(PLSQL_DELIMITER); + + + + + } else { + context.setDelimiter(Delimiter.SEMICOLON); + } + } + + + + + + + + + + + + + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, Token token) { + // Package bodies can have an unbalanced BEGIN without END in the initialisation section. + TokenType tokenType = token.getType(); + if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) { + return true; + } + + // In Oracle, symbols { } affect the block depth in embedded Java code + if (token.getType() == TokenType.SYMBOL && context.getStatementType() == PLSQL_JAVA_STATEMENT) { + return true; + } + + + + + + + + + return super.shouldAdjustBlockDepth(context, token); + } + + // These words increase the block depth - unless preceded by END (in which case the END will decrease the block depth) + private static final List CONTROL_FLOW_KEYWORDS = Arrays.asList("IF", "LOOP", "CASE"); + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + String keywordText = keyword.getText(); + + // In embedded Java code we judge the end of a class definition by the depth of braces. + // We ignore normal SQL keywords as Java code can contain arbitrary identifiers. + if (context.getStatementType() == PLSQL_JAVA_STATEMENT) { + if ("{".equals(keywordText)) { + context.increaseBlockDepth(); + } else if ("}".equals(keywordText)) { + context.decreaseBlockDepth(); + } + return; + } + + int parensDepth = keyword.getParensDepth(); + + if ("BEGIN".equals(keywordText) + || (CONTROL_FLOW_KEYWORDS.contains(keywordText) && !lastTokenIs(tokens, parensDepth, "END")) + || ("TRIGGER".equals(keywordText) && lastTokenIs(tokens, parensDepth, "COMPOUND")) + || doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_BODY_REGEX) + || doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_DEFINITION_REGEX) + || doTokensMatchPattern(tokens, keyword, PLSQL_TYPE_BODY_REGEX) + ) { + context.increaseBlockDepth(); + } else if ("END".equals(keywordText)) { + context.decreaseBlockDepth(); + } + + // Package bodies can have an unbalanced BEGIN without END in the initialisation section. This allows us + // to exit the package even though we are still at block depth 1 due to the BEGIN. + TokenType tokenType = keyword.getType(); + if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType) && context.getBlockDepth() == 1) { + context.decreaseBlockDepth(); + return; + } + } + + @Override + protected boolean isDelimiter(String peek, ParserContext context, int col) { + Delimiter delimiter = context.getDelimiter(); + + // Only consider alone-on-line delimiters (such as "/" for PL/SQL) if + // it's the first character on the line + if (delimiter.isAloneOnLine() && col > 1) { + return false; + } + + + + + + + + if (col == 1 && "/".equals(peek.trim())) { + return true; + } + + return super.isDelimiter(peek, context, col); + } + + + + + + + + + + + + + + + @Override + protected boolean isAlternativeStringLiteral(String peek) { + if (peek.length() < 3) { + return false; + } + // Oracle's quoted-literal syntax is introduced by q (case-insensitive) followed by a literal surrounded by + // any of !!, [], {}, (), <> provided the selected pair do not appear in the literal string; the others may do. + char firstChar = peek.charAt(0); + return (firstChar == 'q' || firstChar == 'Q') && peek.charAt(1) == '\''; + } + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + String closeQuote = computeAlternativeCloseQuote((char) reader.read()); + reader.swallowUntilExcluding(closeQuote); + reader.swallow(closeQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + private String computeAlternativeCloseQuote(char specialChar) { + switch (specialChar) { + case '!': + return "!'"; + case '[': + return "]'"; + case '(': + return ")'"; + case '{': + return "}'"; + case '<': + return ">'"; + default: + return specialChar + "'"; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.java new file mode 100644 index 00000000..baf40080 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.java @@ -0,0 +1,153 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Warning; +import org.flywaydb.core.internal.jdbc.Result; +import org.flywaydb.core.internal.jdbc.Results; + +/** + * Oracle-specific results and side-effects. + */ +public class OracleResults extends Results { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSchema.java new file mode 100644 index 00000000..a811c684 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSchema.java @@ -0,0 +1,873 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.flywaydb.core.internal.database.oracle.OracleSchema.ObjectType.*; + +/** + * Oracle implementation of Schema. + */ +public class OracleSchema extends Schema { + private static final Log LOG = LogFactory.getLog(OracleSchema.class); + + /** + * Creates a new Oracle schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + OracleSchema(JdbcTemplate jdbcTemplate, OracleDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + /** + * Checks whether the schema is system, i.e. Oracle-maintained, or not. + * + * @return {@code true} if it is system, {@code false} if not. + */ + public boolean isSystem() throws SQLException { + return database.getSystemSchemas().contains(name); + } + + /** + * Checks whether this schema is default for the current user. + * + * @return {@code true} if it is default, {@code false} if not. + */ + boolean isDefaultSchemaForUser() throws SQLException { + return name.equals(database.doGetCurrentUser()); + } + + @Override + protected boolean doExists() throws SQLException { + return database.queryReturnsRows("SELECT * FROM ALL_USERS WHERE USERNAME = ?", name); + } + + @Override + protected boolean doEmpty() throws SQLException { + return !supportedTypesExist(jdbcTemplate, database, this); + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE USER " + database.quote(name) + " IDENTIFIED BY " + + database.quote("FFllyywwaayy00!!")); + jdbcTemplate.execute("GRANT RESOURCE TO " + database.quote(name)); + jdbcTemplate.execute("GRANT UNLIMITED TABLESPACE TO " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP USER " + database.quote(name) + " CASCADE"); + } + + @Override + protected void doClean() throws SQLException { + if (isSystem()) { + throw new FlywayException("Clean not supported on Oracle for system schema " + database.quote(name) + "! " + + "It must not be changed in any way except by running an Oracle-supplied script!"); + } + + // Disable FBA for schema tables. + if (database.isFlashbackDataArchiveAvailable()) { + disableFlashbackArchiveForFbaTrackedTables(); + } + + // Clean Oracle Locator metadata. + if (database.isLocatorAvailable()) { + cleanLocatorMetadata(); + } + + // Get existing object types in the schema. + Set objectTypeNames = getObjectTypeNames(jdbcTemplate, database, this); + + // Define the list of types to process, order is important. + List objectTypesToClean = Arrays.asList( + // Types to drop. + TRIGGER, + QUEUE_TABLE, + FILE_WATCHER, + SCHEDULER_CHAIN, + SCHEDULER_JOB, + SCHEDULER_PROGRAM, + SCHEDULE, + RULE_SET, + RULE, + EVALUATION_CONTEXT, + FILE_GROUP, + XML_SCHEMA, + MINING_MODEL, + REWRITE_EQUIVALENCE, + SQL_TRANSLATION_PROFILE, + MATERIALIZED_VIEW, + MATERIALIZED_VIEW_LOG, + DIMENSION, + VIEW, + DOMAIN_INDEX, + DOMAIN_INDEX_TYPE, + TABLE, + INDEX, + CLUSTER, + SEQUENCE, + OPERATOR, + FUNCTION, + PROCEDURE, + PACKAGE, + CONTEXT, + LIBRARY, + TYPE, + SYNONYM, + JAVA_SOURCE, + JAVA_CLASS, + JAVA_RESOURCE, + + // Object types with sensitive information (passwords), skip intentionally, print warning if found. + DATABASE_LINK, + CREDENTIAL, + + // Unsupported types, print warning if found + DATABASE_DESTINATION, + SCHEDULER_GROUP, + CUBE, + CUBE_DIMENSION, + CUBE_BUILD_PROCESS, + MEASURE_FOLDER, + + // Undocumented types, print warning if found + ASSEMBLY, + JAVA_DATA + ); + + for (ObjectType objectType : objectTypesToClean) { + if (objectTypeNames.contains(objectType.getName())) { + LOG.debug("Cleaning objects of type " + objectType + " ..."); + objectType.dropObjects(jdbcTemplate, database, this); + } + } + + if (isDefaultSchemaForUser()) { + jdbcTemplate.execute("PURGE RECYCLEBIN"); + } + } + + /** + * Executes ALTER statements for all tables that have Flashback Archive enabled. + * Flashback Archive is an asynchronous process so we need to wait until it completes, otherwise cleaning the + * tables in schema will sometimes fail with ORA-55622 or ORA-55610 depending on the race between + * Flashback Archive and Java code. + * + * @throws SQLException when the statements could not be generated. + */ + private void disableFlashbackArchiveForFbaTrackedTables() throws SQLException { + boolean dbaViewAccessible = database.isPrivOrRoleGranted("SELECT ANY DICTIONARY") + || database.isDataDictViewAccessible("DBA_FLASHBACK_ARCHIVE_TABLES"); + + if (!dbaViewAccessible && !isDefaultSchemaForUser()) { + LOG.warn("Unable to check and disable Flashback Archive for tables in schema " + database.quote(name) + + " by user \"" + database.doGetCurrentUser() + "\": DBA_FLASHBACK_ARCHIVE_TABLES is not accessible"); + return; + } + + boolean oracle18orNewer = database.getVersion().isAtLeast("18"); + + String queryForFbaTrackedTables = "SELECT TABLE_NAME FROM " + (dbaViewAccessible ? "DBA_" : "USER_") + + "FLASHBACK_ARCHIVE_TABLES WHERE OWNER_NAME = ?" + + (oracle18orNewer ? " AND STATUS='ENABLED'" : ""); + List tableNames = jdbcTemplate.queryForStringList(queryForFbaTrackedTables, name); + for (String tableName : tableNames) { + jdbcTemplate.execute("ALTER TABLE " + database.quote(name, tableName) + " NO FLASHBACK ARCHIVE"); + //wait until the tables disappear + while (database.queryReturnsRows(queryForFbaTrackedTables + " AND TABLE_NAME = ?", name, tableName)) { + try { + LOG.debug("Actively waiting for Flashback cleanup on table: " + database.quote(name, tableName)); + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new FlywayException("Waiting for Flashback cleanup interrupted", e); + } + } + } + + if (oracle18orNewer) { + while (database.queryReturnsRows("SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = ?\n" + + " AND TABLE_NAME LIKE 'SYS_FBA_DDL_COLMAP_%'", name)) { + try { + LOG.debug("Actively waiting for Flashback colmap cleanup"); + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new FlywayException("Waiting for Flashback colmap cleanup interrupted", e); + } + } + } + } + + /** + * Checks whether Oracle Locator metadata exists for the schema. + * + * @return {@code true} if it exists, {@code false} if not. + * @throws SQLException when checking metadata existence failed. + */ + private boolean locatorMetadataExists() throws SQLException { + return database.queryReturnsRows("SELECT * FROM ALL_SDO_GEOM_METADATA WHERE OWNER = ?", name); + } + + /** + * Clean Oracle Locator metadata for the schema. Works only for the user's default schema, prints a warning message + * to log otherwise. + * + * @throws SQLException when performing cleaning failed. + */ + private void cleanLocatorMetadata() throws SQLException { + if (!locatorMetadataExists()) { + return; + } + + if (!isDefaultSchemaForUser()) { + LOG.warn("Unable to clean Oracle Locator metadata for schema " + database.quote(name) + + " by user \"" + database.doGetCurrentUser() + "\": unsupported operation"); + return; + } + + jdbcTemplate.getConnection().commit(); + jdbcTemplate.execute("DELETE FROM USER_SDO_GEOM_METADATA"); + jdbcTemplate.getConnection().commit(); + } + + @Override + protected OracleTable[] doAllTables() throws SQLException { + List tableNames = TABLE.getObjectNames(jdbcTemplate, database, this); + + OracleTable[] tables = new OracleTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new OracleTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new OracleTable(jdbcTemplate, database, this, tableName); + } + + + /** + * Oracle object types. + */ + public enum ObjectType { + // Tables, including XML tables, except for nested tables, IOT overflow tables and other secondary objects. + TABLE("TABLE", "CASCADE CONSTRAINTS PURGE") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + boolean referencePartitionedTablesExist = database.queryReturnsRows( + "SELECT * FROM ALL_PART_TABLES WHERE OWNER = ? AND PARTITIONING_TYPE = 'REFERENCE'", + schema.getName()); + boolean xmlDbAvailable = database.isXmlDbAvailable(); + + StringBuilder tablesQuery = new StringBuilder(); + tablesQuery.append("WITH TABLES AS (\n" + + " SELECT TABLE_NAME, OWNER\n" + + " FROM ALL_TABLES\n" + + " WHERE OWNER = ?\n" + + " AND (IOT_TYPE IS NULL OR IOT_TYPE NOT LIKE '%OVERFLOW%')\n" + + " AND NESTED != 'YES'\n" + + " AND SECONDARY != 'Y'\n"); + + if (xmlDbAvailable) { + tablesQuery.append(" UNION ALL\n" + + " SELECT TABLE_NAME, OWNER\n" + + " FROM ALL_XML_TABLES\n" + + " WHERE OWNER = ?\n" + + // ALL_XML_TABLES shows objects in RECYCLEBIN, ignore them + " AND TABLE_NAME NOT LIKE 'BIN$________________________$_'\n"); + } + + tablesQuery.append(")\n" + + "SELECT t.TABLE_NAME\n" + + "FROM TABLES t\n"); + + // Reference partitioned tables should be dropped in child-to-parent order. + if (referencePartitionedTablesExist) { + tablesQuery.append(" LEFT JOIN ALL_PART_TABLES pt\n" + + " ON t.OWNER = pt.OWNER\n" + + " AND t.TABLE_NAME = pt.TABLE_NAME\n" + + " AND pt.PARTITIONING_TYPE = 'REFERENCE'\n" + + " LEFT JOIN ALL_CONSTRAINTS fk\n" + + " ON pt.OWNER = fk.OWNER\n" + + " AND pt.TABLE_NAME = fk.TABLE_NAME\n" + + " AND pt.REF_PTN_CONSTRAINT_NAME = fk.CONSTRAINT_NAME\n" + + " AND fk.CONSTRAINT_TYPE = 'R'\n" + + " LEFT JOIN ALL_CONSTRAINTS puk\n" + + " ON fk.R_OWNER = puk.OWNER\n" + + " AND fk.R_CONSTRAINT_NAME = puk.CONSTRAINT_NAME\n" + + " AND puk.CONSTRAINT_TYPE IN ('P', 'U')\n" + + " LEFT JOIN TABLES p\n" + + " ON puk.OWNER = p.OWNER\n" + + " AND puk.TABLE_NAME = p.TABLE_NAME\n" + + "START WITH p.TABLE_NAME IS NULL\n" + + "CONNECT BY PRIOR t.TABLE_NAME = p.TABLE_NAME\n" + + "ORDER BY LEVEL DESC"); + } + + int n = 1 + (xmlDbAvailable ? 1 : 0); + String[] params = new String[n]; + Arrays.fill(params, schema.getName()); + + return jdbcTemplate.queryForStringList(tablesQuery.toString(), params); + } + }, + + // Queue tables, have related objects and should be dropped separately prior to other types. + QUEUE_TABLE("QUEUE TABLE") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT QUEUE_TABLE FROM ALL_QUEUE_TABLES WHERE OWNER = ?", + schema.getName() + ); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_AQADM.DROP_QUEUE_TABLE('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // Materialized view logs. + MATERIALIZED_VIEW_LOG("MATERIALIZED VIEW LOG") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT MASTER FROM ALL_MVIEW_LOGS WHERE LOG_OWNER = ?", + schema.getName() + ); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "DROP " + this.getName() + " ON " + database.quote(schema.getName(), objectName); + } + }, + + // All indexes, except for domain indexes, should be dropped after tables (if any left). + INDEX("INDEX") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT INDEX_NAME FROM ALL_INDEXES WHERE OWNER = ?" + + //" AND INDEX_NAME NOT LIKE 'SYS_C%'"+ + " AND INDEX_TYPE NOT LIKE '%DOMAIN%'", + schema.getName() + ); + } + }, + + // Domain indexes, have related objects and should be dropped separately prior to tables. + DOMAIN_INDEX("INDEX", "FORCE") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT INDEX_NAME FROM ALL_INDEXES WHERE OWNER = ? AND INDEX_TYPE LIKE '%DOMAIN%'", + schema.getName() + ); + } + }, + + // Domain index types. + DOMAIN_INDEX_TYPE("INDEXTYPE", "FORCE"), + + // Operators. + OPERATOR("OPERATOR", "FORCE"), + + // Clusters. + CLUSTER("CLUSTER", "INCLUDING TABLES CASCADE CONSTRAINTS"), + + // Views, including XML views. + VIEW("VIEW", "CASCADE CONSTRAINTS"), + + // Materialized views, keep tables as they may be referenced. + MATERIALIZED_VIEW("MATERIALIZED VIEW", "PRESERVE TABLE"), + + // Dimensions. + DIMENSION("DIMENSION") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT DIMENSION_NAME FROM ALL_DIMENSIONS WHERE OWNER = ?", + schema.getName() + ); + } + }, + + // Local synonyms. + SYNONYM("SYNONYM", "FORCE"), + + // Sequences, no filtering for identity sequences, since they get dropped along with master tables. + SEQUENCE("SEQUENCE"), + + // Procedures, functions, packages. + PROCEDURE("PROCEDURE"), + FUNCTION("FUNCTION"), + PACKAGE("PACKAGE"), + + // Contexts, seen in DBA_CONTEXT view, may remain if DBA_CONTEXT is not accessible. + CONTEXT("CONTEXT") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT NAMESPACE FROM " + database.dbaOrAll("CONTEXT") + " WHERE SCHEMA = ?", + schema.getName() + ); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "DROP " + this.getName() + " " + database.quote(objectName); // no owner + } + }, + + // Triggers of all types, should be dropped at first, because invalid DDL triggers may break the whole clean. + TRIGGER("TRIGGER"), + + // Types. + TYPE("TYPE", "FORCE"), + + // Java sources, classes, resources. + JAVA_SOURCE("JAVA SOURCE"), + JAVA_CLASS("JAVA CLASS"), + JAVA_RESOURCE("JAVA RESOURCE"), + + // Libraries. + LIBRARY("LIBRARY"), + + // XML schemas. + XML_SCHEMA("XML SCHEMA") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + if (!database.isXmlDbAvailable()) { + return Collections.emptyList(); + } + return jdbcTemplate.queryForStringList( + "SELECT QUAL_SCHEMA_URL FROM " + database.dbaOrAll("XML_SCHEMAS") + " WHERE OWNER = ?", + schema.getName() + ); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_XMLSCHEMA.DELETESCHEMA('" + objectName + "', DELETE_OPTION => DBMS_XMLSCHEMA.DELETE_CASCADE_FORCE); END;"; + } + }, + + // Rewrite equivalences. + REWRITE_EQUIVALENCE("REWRITE EQUIVALENCE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN SYS.DBMS_ADVANCED_REWRITE.DROP_REWRITE_EQUIVALENCE('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + // SQL translation profiles. + SQL_TRANSLATION_PROFILE("SQL TRANSLATION PROFILE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SQL_TRANSLATOR.DROP_PROFILE('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + // Data mining models, have related objects, should be dropped prior to tables. + + + + MINING_MODEL("MINING MODEL") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + + + + return super.getObjectNames(jdbcTemplate, database, schema); + + + + + + + + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_DATA_MINING.DROP_MODEL('" + + + + + database.quote(schema.getName(), objectName) + + + + + "'); END;"; + } + }, + + // Scheduler objects. + SCHEDULER_JOB("JOB") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_JOB('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + SCHEDULER_PROGRAM("PROGRAM") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_PROGRAM('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + SCHEDULE("SCHEDULE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_SCHEDULE('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + SCHEDULER_CHAIN("CHAIN") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_CHAIN('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + FILE_WATCHER("FILE WATCHER") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_FILE_WATCHER('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // Streams/rule objects. + RULE_SET("RULE SET") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_RULE_SET('" + database.quote(schema.getName(), objectName) + "', DELETE_RULES => FALSE); END;"; + } + }, + RULE("RULE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_RULE('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + EVALUATION_CONTEXT("EVALUATION CONTEXT") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_EVALUATION_CONTEXT('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + FILE_GROUP("FILE GROUP") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_FILE_GROUP.DROP_FILE_GROUP('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + + /*** Below are unsupported object types. They should be dropped explicitly in callbacks if used. ***/ + + // Database links and credentials, contain sensitive information (password) and hence not always can be re-created. + // Intentionally skip them and let the clean callbacks handle them if needed. + DATABASE_LINK("DATABASE LINK") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT DB_LINK FROM " + database.dbaOrAll("DB_LINKS") + " WHERE OWNER = ?", + schema.getName() + ); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "DROP " + this.getName() + " " + objectName; // db link name is case-insensitive and needs no owner + } + }, + CREDENTIAL("CREDENTIAL") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_CREDENTIAL('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // Some scheduler types, not supported yet. + DATABASE_DESTINATION("DESTINATION") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_DATABASE_DESTINATION('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + SCHEDULER_GROUP("SCHEDULER GROUP") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_GROUP('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // OLAP objects, not supported yet. + CUBE("CUBE") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + CUBE_DIMENSION("CUBE DIMENSION") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + CUBE_BUILD_PROCESS("CUBE BUILD PROCESS") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName()), "cube build processes"); + } + }, + MEASURE_FOLDER("MEASURE FOLDER") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + + // Undocumented objects. + ASSEMBLY("ASSEMBLY") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName()), "assemblies"); + } + }, + JAVA_DATA("JAVA DATA") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + + // SYS-owned objects, cannot be dropped when a schema gets cleaned, simply ignore them. + CAPTURE("CAPTURE"), + APPLY("APPLY"), + DIRECTORY("DIRECTORY"), + RESOURCE_PLAN("RESOURCE PLAN"), + CONSUMER_GROUP("CONSUMER GROUP"), + JOB_CLASS("JOB CLASS"), + WINDOWS("WINDOW"), + EDITION("EDITION"), + AGENT_DESTINATION("DESTINATION"), + UNIFIED_AUDIT_POLICY("UNIFIED AUDIT POLICY"); + + /** + * The name of the type as it mentioned in the Data Dictionary and the DROP statement. + */ + private final String name; + + /** + * The extra options used in the DROP statement to enforce the operation. + */ + private final String dropOptions; + + ObjectType(String name, String dropOptions) { + this.name = name; + this.dropOptions = dropOptions; + } + + ObjectType(String name) { + this(name, ""); + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return super.toString().replace('_', ' '); + } + + /** + * Returns the list of object names of this type. + * + * @throws SQLException if retrieving of objects failed. + */ + public List getObjectNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList( + "SELECT DISTINCT OBJECT_NAME FROM ALL_OBJECTS WHERE OWNER = ? AND OBJECT_TYPE = ?", + schema.getName(), this.getName() + ); + } + + /** + * Generates the drop statement for the specified object. + * + */ + public String generateDropStatement(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String objectName) { + return "DROP " + this.getName() + " " + database.quote(schema.getName(), objectName) + + (StringUtils.hasText(dropOptions) ? " " + dropOptions : ""); + } + + /** + * Drops all objects of this type in the specified schema. + * + * @throws SQLException if cleaning failed. + */ + public void dropObjects(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + for (String objectName : getObjectNames(jdbcTemplate, database, schema)) { + jdbcTemplate.execute(generateDropStatement(jdbcTemplate, database, schema, objectName)); + } + } + + private void warnUnsupported(String schemaName, String typeDesc) { + LOG.warn("Unable to clean " + typeDesc + " for schema " + schemaName + ": unsupported operation"); + } + + private void warnUnsupported(String schemaName) { + warnUnsupported(schemaName, this.toString().toLowerCase() + "s"); + } + + /** + * Returns the schema's existing object types. + * + * @return a set of object type names. + * @throws SQLException if retrieving of object types failed. + */ + public static Set getObjectTypeNames(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + boolean xmlDbAvailable = database.isXmlDbAvailable(); + + + + + + + + + String query = + // Most object types can be correctly selected from DBA_/ALL_OBJECTS. + "SELECT DISTINCT OBJECT_TYPE FROM " + database.dbaOrAll("OBJECTS") + " WHERE OWNER = ? " + + // Materialized view logs. + "UNION SELECT '" + MATERIALIZED_VIEW_LOG.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM ALL_MVIEW_LOGS WHERE LOG_OWNER = ?) " + + // Dimensions. + "UNION SELECT '" + DIMENSION.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM ALL_DIMENSIONS WHERE OWNER = ?) " + + // Queue tables. + "UNION SELECT '" + QUEUE_TABLE.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM ALL_QUEUE_TABLES WHERE OWNER = ?) " + + // Database links. + "UNION SELECT '" + DATABASE_LINK.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM " + database.dbaOrAll("DB_LINKS") + " WHERE OWNER = ?) " + + // Contexts. + "UNION SELECT '" + CONTEXT.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM " + database.dbaOrAll("CONTEXT") + " WHERE SCHEMA = ?) " + + // XML schemas. + (xmlDbAvailable + ? "UNION SELECT '" + XML_SCHEMA.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM " + database.dbaOrAll("XML_SCHEMAS") + " WHERE OWNER = ?) " + : "") + + // Credentials. + + + + "UNION SELECT '" + CREDENTIAL.getName() + "' FROM DUAL WHERE EXISTS(" + + "SELECT * FROM ALL_SCHEDULER_CREDENTIALS WHERE OWNER = ?) " + + + + + + + + + ; + + int n = 6 + (xmlDbAvailable ? 1 : 0) + + + + + 1 + + + + ; + String[] params = new String[n]; + Arrays.fill(params, schema.getName()); + + return new HashSet<>(jdbcTemplate.queryForStringList(query, params)); + } + + /** + * Checks whether the specified schema contains object types that can be cleaned. + * + * @return {@code true} if it contains, {@code false} if not. + * @throws SQLException if retrieving of object types failed. + */ + public static boolean supportedTypesExist(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema) throws SQLException { + Set existingTypeNames = new HashSet<>(getObjectTypeNames(jdbcTemplate, database, schema)); + + // Remove unsupported types. + existingTypeNames.removeAll(Arrays.asList( + DATABASE_LINK.getName(), + CREDENTIAL.getName(), + DATABASE_DESTINATION.getName(), + SCHEDULER_GROUP.getName(), + CUBE.getName(), + CUBE_DIMENSION.getName(), + CUBE_BUILD_PROCESS.getName(), + MEASURE_FOLDER.getName(), + ASSEMBLY.getName(), + JAVA_DATA.getName() + )); + + return !existingTypeNames.isEmpty(); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.java new file mode 100644 index 00000000..ee6cea5f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.java @@ -0,0 +1,331 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.callback.CallbackExecutor; + +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.JdbcUtils; +import org.flywaydb.core.internal.jdbc.Result; +import org.flywaydb.core.internal.jdbc.Results; +import org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlStatement; +import org.flywaydb.core.internal.util.AsciiTable; +import org.flywaydb.core.internal.util.DateUtils; +import org.flywaydb.core.internal.util.StopWatch; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Array; +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@SuppressWarnings("SqlResolve") +public class OracleSqlScriptExecutor extends DefaultSqlScriptExecutor { + + + + + + + + + + + + + + + + + + + + public OracleSqlScriptExecutor(JdbcTemplate jdbcTemplate + + + + + ) { + super(jdbcTemplate + + + + ); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleTable.java new file mode 100644 index 00000000..d171523e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleTable.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.oracle; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Oracle-specific table. + */ +public class OracleTable extends Table { + /** + * Creates a new Oracle table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public OracleTable(JdbcTemplate jdbcTemplate, OracleDatabase database, OracleSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE CONSTRAINTS PURGE"); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("LOCK TABLE " + this + " IN EXCLUSIVE MODE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/package-info.java new file mode 100644 index 00000000..f48f94a9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.oracle; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/package-info.java new file mode 100644 index 00000000..51fdfbf6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLAdvisoryLockTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLAdvisoryLockTemplate.java new file mode 100644 index 00000000..d70b2ede --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLAdvisoryLockTemplate.java @@ -0,0 +1,117 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.jdbc.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing with PostgreSQL advisory locks. + */ +public class PostgreSQLAdvisoryLockTemplate { + private static final Log LOG = LogFactory.getLog(PostgreSQLAdvisoryLockTemplate.class); + + private static final long LOCK_MAGIC_NUM = + (0x46L << 40) // F + + (0x6CL << 32) // l + + (0x79L << 24) // y + + (0x77 << 16) // w + + (0x61 << 8) // a + + 0x79; // y + + /** + * The connection for the advisory lock. + */ + private final JdbcTemplate jdbcTemplate; + + private final long lockNum; + + /** + * Creates a new advisory lock template for this connection. + * + * @param jdbcTemplate The jdbcTemplate for the connection. + * @param discriminator A number to discriminate between locks. + */ + PostgreSQLAdvisoryLockTemplate(JdbcTemplate jdbcTemplate, int discriminator) { + this.jdbcTemplate = jdbcTemplate; + lockNum = LOCK_MAGIC_NUM + discriminator; + } + + /** + * Executes this callback with an advisory lock. + * + * @param callable The callback to execute. + * @return The result of the callable code. + */ + public T execute(Callable callable) { + try { + lock(); + return callable.call(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to acquire PostgreSQL advisory lock", e); + } catch (Exception e) { + RuntimeException rethrow; + if (e instanceof RuntimeException) { + rethrow = (RuntimeException) e; + } else { + rethrow = new FlywayException(e); + } + throw rethrow; + } finally { + try { + jdbcTemplate.execute("SELECT pg_advisory_unlock(" + lockNum + ")"); + } catch (SQLException e) { + throw new FlywayException("Unable to release PostgreSQL advisory lock", e); + } + } + } + + private void lock() throws SQLException { + int retries = 0; + while (!tryLock()) { + try { + Thread.sleep(100L); + } catch (InterruptedException e) { + throw new FlywayException("Interrupted while attempting to acquire PostgreSQL advisory lock", e); + } + + if (++retries >= 50) { + throw new FlywayException("Number of retries exceeded while attempting to acquire PostgreSQL advisory lock"); + } + } + } + + private boolean tryLock() throws SQLException { + List results = jdbcTemplate.query( + "SELECT pg_try_advisory_lock(" + lockNum + ")", + new RowMapper() { + @Override + public Boolean mapRow(ResultSet rs) throws SQLException { + return rs.getBoolean("pg_try_advisory_lock"); + } + }); + return results.size() == 1 && results.get(0); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.java new file mode 100644 index 00000000..0edcb638 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.concurrent.Callable; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.util.StringUtils; + +/** + * PostgreSQL connection. + */ +public class PostgreSQLConnection extends Connection { + private final String originalRole; + + PostgreSQLConnection(PostgreSQLDatabase database, java.sql.Connection connection) { + super(database, connection); + + try { + originalRole = jdbcTemplate.queryForString("SELECT CURRENT_USER"); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine current user", e); + } + } + +// @Override +// protected void doRestoreOriginalState() throws SQLException { +// // Reset the role to its original value in case a migration or callback changed it +// // 由于 postgresql 和 高斯 之间对设置 role 语法之间的差异,高斯数据库 set 角色时还需要带上密码,而postgresql则不用 +// jdbcTemplate.execute("SET ROLE '" + originalRole + "'"); +// } + + @Override + public Schema doGetCurrentSchema() throws SQLException { + String currentSchema = jdbcTemplate.queryForString("SELECT current_schema"); + String searchPath = getCurrentSchemaNameOrSearchPath(); + + if (!StringUtils.hasText(currentSchema) && !StringUtils.hasText(searchPath)) { + throw new FlywayException("Unable to determine current schema as search_path is empty. " + + "Set the current schema in currentSchema parameter of the JDBC URL or in Flyway's schemas property."); + } + + String schema = StringUtils.hasText(currentSchema) ? currentSchema : searchPath; + + return getSchema(schema); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SHOW search_path"); + } + + @Override + public void changeCurrentSchemaTo(Schema schema) { + try { + if (schema.getName().equals(originalSchemaNameOrSearchPath) || originalSchemaNameOrSearchPath.startsWith(schema.getName() + ",") || !schema.exists()) { + return; + } + + if (StringUtils.hasText(originalSchemaNameOrSearchPath)) { + doChangeCurrentSchemaOrSearchPathTo(schema.toString() + "," + originalSchemaNameOrSearchPath); + } else { + doChangeCurrentSchemaOrSearchPathTo(schema.toString()); + } + } catch (SQLException e) { + throw new FlywaySqlException("Error setting current schema to " + schema, e); + } + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SELECT set_config('search_path', ?, false)", schema); + } + + @Override + public Schema getSchema(String name) { + return new PostgreSQLSchema(jdbcTemplate, database, name); + } + + @Override + public T lock(Table table, Callable callable) { + return new PostgreSQLAdvisoryLockTemplate(jdbcTemplate, table.toString().hashCode()).execute(callable); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.java new file mode 100644 index 00000000..dabf132e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.Result; +import org.flywaydb.core.internal.jdbc.Results; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutor; + +import java.io.Reader; +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * A PostgreSQL COPY FROM STDIN statement. + */ +public class PostgreSQLCopyParsedStatement extends ParsedSqlStatement { + /** + * Delimiter of COPY statements. + */ + private static final Delimiter COPY_DELIMITER = new Delimiter("\\.", true); + + private final String copyData; + + /** + * Creates a new PostgreSQL COPY ... FROM STDIN statement. + */ + public PostgreSQLCopyParsedStatement(int pos, int line, int col, String sql, String copyData) { + super(pos, line, col, sql, COPY_DELIMITER, true); + this.copyData = copyData; + } + + @Override + public Results execute(JdbcTemplate jdbcTemplate) { + // #2355: Use reflection to ensure this works in cases where the PostgreSQL driver classes were loaded in a + // child URLClassLoader instead of the system classloader. + Object baseConnection; + Object copyManager; + Method copyManagerCopyInMethod; + try { + Connection connection = jdbcTemplate.getConnection(); + ClassLoader classLoader = connection.getClass().getClassLoader(); + + Class baseConnectionClass = classLoader.loadClass("org.postgresql.core.BaseConnection"); + baseConnection = connection.unwrap(baseConnectionClass); + + Class copyManagerClass = classLoader.loadClass("org.postgresql.copy.CopyManager"); + Constructor copyManagerConstructor = copyManagerClass.getConstructor(baseConnectionClass); + copyManagerCopyInMethod = copyManagerClass.getMethod("copyIn", String.class, Reader.class); + + copyManager = copyManagerConstructor.newInstance(baseConnection); + } catch (Exception e) { + throw new FlywayException("Unable to find PostgreSQL CopyManager class", e); + } + + Results results = new Results(); + try { + try { + Long updateCount = (Long) copyManagerCopyInMethod.invoke(copyManager, getSql(), new StringReader(copyData)); + results.addResult(new Result(updateCount, null, null, getSql())); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new SQLException("Unable to execute COPY operation", e); + } + } catch (SQLException e) { + jdbcTemplate.extractErrors(results, e); + } + return results; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java new file mode 100644 index 00000000..585f5fd4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java @@ -0,0 +1,149 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * PostgreSQL database. + */ +public class PostgreSQLDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public PostgreSQLDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory) { + super(configuration, jdbcConnectionFactory); + } + + @Override + protected PostgreSQLConnection doGetConnection(Connection connection) { + return new PostgreSQLConnection(this, connection); + } + + @Override + public final void ensureSupported() { + ensureDatabaseIsRecentEnough("9.0"); + // 高斯数据库是基于 postgresql 9.2 改造的 + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("9.0", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessaryForMajorVersion("12"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = configuration.getTablespace() == null + ? "" + : " TABLESPACE \"" + configuration.getTablespace() + "\""; + + String createTableScript = + "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INTEGER,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT now(),\n" + + " \"execution_time\" INTEGER NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ")" + tablespace + ";\n"; + + if (baseline) { + return createTableScript + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");\n" + + "CREATE INDEX \"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } else { + return createTableScript; + } + + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT current_user"); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "TRUE"; + } + + @Override + public String getBooleanFalse() { + return "FALSE"; + } + + @Override + public String doQuote(String identifier) { + return pgQuote(identifier); + } + + static String pgQuote(String identifier) { + return "\"" + StringUtils.replaceAll(identifier, "\"", "\"\"") + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return true; + } + + /** + * This exists to fix this issue: https://github.com/flyway/flyway/issues/2638 + * See https://www.pgpool.net/docs/latest/en/html/runtime-config-load-balancing.html + */ + @Override + public String getSelectStatement(Table table) { + return "/*NO LOAD BALANCE*/\n" + + "SELECT " + quote("installed_rank") + + "," + quote("version") + + "," + quote("description") + + "," + quote("type") + + "," + quote("script") + + "," + quote("checksum") + + "," + quote("installed_on") + + "," + quote("installed_by") + + "," + quote("execution_time") + + "," + quote("success") + + " FROM " + table + + " WHERE " + quote("installed_rank") + " > ?" + + " ORDER BY " + quote("installed_rank"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLParser.java new file mode 100644 index 00000000..b59d0f1f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLParser.java @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +public class PostgreSQLParser extends Parser { + private static final Pattern COPY_FROM_STDIN_REGEX = Pattern.compile("^COPY( .*)? FROM STDIN"); + private static final Pattern CREATE_DATABASE_TABLESPACE_SUBSCRIPTION_REGEX = Pattern.compile("^(CREATE|DROP) (DATABASE|TABLESPACE|SUBSCRIPTION)"); + private static final Pattern ALTER_SYSTEM_REGEX = Pattern.compile("^ALTER SYSTEM"); + private static final Pattern CREATE_INDEX_CONCURRENTLY_REGEX = Pattern.compile("^(CREATE|DROP)( UNIQUE)? INDEX CONCURRENTLY"); + private static final Pattern REINDEX_REGEX = Pattern.compile("^REINDEX( VERBOSE)? (SCHEMA|DATABASE|SYSTEM)"); + private static final Pattern VACUUM_REGEX = Pattern.compile("^VACUUM"); + private static final Pattern DISCARD_ALL_REGEX = Pattern.compile("^DISCARD ALL"); + private static final Pattern ALTER_TYPE_ADD_VALUE_REGEX = Pattern.compile("^ALTER TYPE( .*)? ADD VALUE"); + + private static final StatementType COPY = new StatementType(); + + public PostgreSQLParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '$'; + } + + @Override + protected ParsedSqlStatement createStatement(PeekingReader reader, Recorder recorder, + int statementPos, int statementLine, int statementCol, + int nonCommentPartPos, int nonCommentPartLine, int nonCommentPartCol, + StatementType statementType, boolean canExecuteInTransaction, + Delimiter delimiter, String sql + + + + ) throws IOException { + if (statementType == COPY) { + return new PostgreSQLCopyParsedStatement(nonCommentPartPos, nonCommentPartLine, nonCommentPartCol, + sql.substring(nonCommentPartPos - statementPos), + readCopyData(reader, recorder)); + } + return super.createStatement(reader, recorder, statementPos, statementLine, statementCol, + nonCommentPartPos, nonCommentPartLine, nonCommentPartCol, + statementType, canExecuteInTransaction, delimiter, sql + + + + ); + } + + private String readCopyData(PeekingReader reader, Recorder recorder) throws IOException { + // Skip end of current line after ; + reader.readUntilIncluding('\n'); + + recorder.start(); + boolean done = false; + do { + String line = reader.readUntilIncluding('\n'); + if ("\\.".equals(line.trim())) { + done = true; + } else { + recorder.confirm(); + } + } while (!done); + + return recorder.stop(); + } + + @Override + protected StatementType detectStatementType(String simplifiedStatement) { + if (COPY_FROM_STDIN_REGEX.matcher(simplifiedStatement).matches()) { + return COPY; + } + + return super.detectStatementType(simplifiedStatement); + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + if (CREATE_DATABASE_TABLESPACE_SUBSCRIPTION_REGEX.matcher(simplifiedStatement).matches() + || ALTER_SYSTEM_REGEX.matcher(simplifiedStatement).matches() + || CREATE_INDEX_CONCURRENTLY_REGEX.matcher(simplifiedStatement).matches() + || REINDEX_REGEX.matcher(simplifiedStatement).matches() + || VACUUM_REGEX.matcher(simplifiedStatement).matches() + || DISCARD_ALL_REGEX.matcher(simplifiedStatement).matches() + || ALTER_TYPE_ADD_VALUE_REGEX.matcher(simplifiedStatement).matches()) { + return false; + } + + return null; + } + + @SuppressWarnings("Duplicates") + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + // dollarQuote is required because in Postgres, literals encased in $$ can be given a label, as in: + // $label$This is a string literal$label$ + String dollarQuote = (char) reader.read() + reader.readUntilIncluding('$'); + reader.swallowUntilExcluding(dollarQuote); + reader.swallow(dollarQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java new file mode 100644 index 00000000..c94b02d7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java @@ -0,0 +1,346 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.database.base.Type; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * PostgreSQL implementation of Schema. + */ +public class PostgreSQLSchema extends Schema { + /** + * Creates a new PostgreSQL schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + PostgreSQLSchema(JdbcTemplate jdbcTemplate, PostgreSQLDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM pg_namespace WHERE nspname=?", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + return !jdbcTemplate.queryForBoolean("SELECT EXISTS (\n" + + " SELECT c.oid FROM pg_catalog.pg_class c\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + + " LEFT JOIN pg_catalog.pg_depend d ON d.objid = c.oid AND d.deptype = 'e'\n" + + " WHERE n.nspname = ? AND d.objid IS NULL AND c.relkind IN ('r', 'v', 'S', 't')\n" + + " UNION ALL\n" + + " SELECT t.oid FROM pg_catalog.pg_type t\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\n" + + " LEFT JOIN pg_catalog.pg_depend d ON d.objid = t.oid AND d.deptype = 'e'\n" + + " WHERE n.nspname = ? AND d.objid IS NULL AND t.typcategory NOT IN ('A', 'C')\n" + + " UNION ALL\n" + + " SELECT p.oid FROM pg_catalog.pg_proc p\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n" + + " LEFT JOIN pg_catalog.pg_depend d ON d.objid = p.oid AND d.deptype = 'e'\n" + + " WHERE n.nspname = ? AND d.objid IS NULL\n" + + ")", name, name, name); + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " CASCADE"); + } + + @Override + protected void doClean() throws SQLException { + + + + for (String statement : generateDropStatementsForMaterializedViews()) { + jdbcTemplate.execute(statement); + } + + + + + for (String statement : generateDropStatementsForViews()) { + jdbcTemplate.execute(statement); + } + + for (Table table : allTables()) { + table.drop(); + } + + for (String statement : generateDropStatementsForBaseTypes(true)) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForRoutines()) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForEnums()) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForDomains()) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForSequences()) { + jdbcTemplate.execute(statement); + } + + for (String statement : generateDropStatementsForBaseTypes(false)) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generates the statements for dropping the sequences in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForSequences() throws SQLException { + List sequenceNames = + jdbcTemplate.queryForStringList( + "SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema=?", name); + + List statements = new ArrayList<>(); + for (String sequenceName : sequenceNames) { + statements.add("DROP SEQUENCE IF EXISTS " + database.quote(name, sequenceName)); + } + + return statements; + } + + /** + * Generates the statements for dropping the types in this schema. + * + * @param recreate Flag indicating whether the types should be recreated. Necessary for type-function chicken and egg problem. + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForBaseTypes(boolean recreate) throws SQLException { + List> rows = + jdbcTemplate.queryForList( + "select typname, typcategory from pg_catalog.pg_type t " + + "left join pg_depend dep on dep.objid = t.oid and dep.deptype = 'e' " + + "where (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid)) " + + "and NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) " + + "and t.typnamespace in (select oid from pg_catalog.pg_namespace where nspname = ?) " + + "and dep.objid is null " + + "and t.typtype != 'd'", + name); + + List statements = new ArrayList<>(); + for (Map row : rows) { + statements.add("DROP TYPE IF EXISTS " + database.quote(name, row.get("typname")) + " CASCADE"); + } + + if (recreate) { + for (Map row : rows) { + // Only recreate Pseudo-types (P) and User-defined types (U) + if (Arrays.asList("P", "U").contains(row.get("typcategory"))) { + statements.add("CREATE TYPE " + database.quote(name, row.get("typname"))); + } + } + } + + return statements; + } + + /** + * Generates the statements for dropping the routines in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForRoutines() throws SQLException { + // #2193: PostgreSQL 11 removed the 'proisagg' column and replaced it with 'prokind'. + String isAggregate = database.getVersion().isAtLeast("11") ? "pg_proc.prokind = 'a'" : "pg_proc.proisagg"; + // PROCEDURE is only available from PostgreSQL 11 + String isProcedure = database.getVersion().isAtLeast("11") ? "pg_proc.prokind = 'p'" : "FALSE"; + + List> rows = + jdbcTemplate.queryForList( + // Search for all functions + "SELECT proname, oidvectortypes(proargtypes) AS args, " + isAggregate + " as agg, " + isProcedure + " as proc " + + "FROM pg_proc INNER JOIN pg_namespace ns ON (pg_proc.pronamespace = ns.oid) " + // that don't depend on an extension + + "LEFT JOIN pg_depend dep ON dep.objid = pg_proc.oid AND dep.deptype = 'e' " + + "WHERE ns.nspname = ? AND dep.objid IS NULL", + name + ); + + List statements = new ArrayList<>(); + for (Map row : rows) { + String type = "FUNCTION"; + if (isTrue(row.get("agg"))) { + type = "AGGREGATE"; + } else if (isTrue(row.get("proc"))) { + type = "PROCEDURE"; + } + statements.add("DROP " + type + " IF EXISTS " + + database.quote(name, row.get("proname")) + "(" + row.get("args") + ") CASCADE"); + } + return statements; + } + + private boolean isTrue(String agg) { + return agg != null && agg.toLowerCase(Locale.ENGLISH).startsWith("t"); + } + + /** + * Generates the statements for dropping the enums in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForEnums() throws SQLException { + List enumNames = + jdbcTemplate.queryForStringList( + "SELECT t.typname FROM pg_catalog.pg_type t INNER JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = ? and t.typtype = 'e'", name); + + List statements = new ArrayList<>(); + for (String enumName : enumNames) { + statements.add("DROP TYPE " + database.quote(name, enumName)); + } + + return statements; + } + + /** + * Generates the statements for dropping the domains in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForDomains() throws SQLException { + List domainNames = + jdbcTemplate.queryForStringList( + "SELECT t.typname as domain_name\n" + + "FROM pg_catalog.pg_type t\n" + + " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\n" + + " LEFT JOIN pg_depend dep ON dep.objid = t.oid AND dep.deptype = 'e'\n" + + "WHERE t.typtype = 'd'\n" + + " AND n.nspname = ?\n" + + " AND dep.objid IS NULL" + , name); + + List statements = new ArrayList<>(); + for (String domainName : domainNames) { + statements.add("DROP DOMAIN " + database.quote(name, domainName)); + } + + return statements; + } + + /** + * Generates the statements for dropping the materialized views in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForMaterializedViews() throws SQLException { + List viewNames = + jdbcTemplate.queryForStringList( + "SELECT relname FROM pg_catalog.pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace" + + " WHERE c.relkind = 'm' AND n.nspname = ?", name); + + List statements = new ArrayList<>(); + for (String domainName : viewNames) { + statements.add("DROP MATERIALIZED VIEW IF EXISTS " + database.quote(name, domainName) + " CASCADE"); + } + + return statements; + } + + /** + * Generates the statements for dropping the views in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForViews() throws SQLException { + List viewNames = + jdbcTemplate.queryForStringList( + // Search for all views + "SELECT relname FROM pg_catalog.pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace" + + // that don't depend on an extension + " LEFT JOIN pg_depend dep ON dep.objid = c.oid AND dep.deptype = 'e'" + + " WHERE c.relkind = 'v' AND n.nspname = ? AND dep.objid IS NULL", + name); + List statements = new ArrayList<>(); + for (String domainName : viewNames) { + statements.add("DROP VIEW IF EXISTS " + database.quote(name, domainName) + " CASCADE"); + } + + return statements; + } + + @Override + protected PostgreSQLTable[] doAllTables() throws SQLException { + List tableNames = + jdbcTemplate.queryForStringList( + //Search for all the table names + "SELECT t.table_name FROM information_schema.tables t" + + // that don't depend on an extension + " LEFT JOIN pg_depend dep ON dep.objid = (quote_ident(t.table_schema)||'.'||quote_ident(t.table_name))::regclass::oid AND dep.deptype = 'e'" + + // in this schema + " WHERE table_schema=?" + + //that are real tables (as opposed to views) + " AND table_type='BASE TABLE'" + + // with no extension depending on them + " AND dep.objid IS NULL" + + // and are not child tables (= do not inherit from another table). + " AND NOT (SELECT EXISTS (SELECT inhrelid FROM pg_catalog.pg_inherits" + + " WHERE inhrelid = (quote_ident(t.table_schema)||'.'||quote_ident(t.table_name))::regclass::oid))", + name + ); + //Views and child tables are excluded as they are dropped with the parent table when using cascade. + + PostgreSQLTable[] tables = new PostgreSQLTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new PostgreSQLTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new PostgreSQLTable(jdbcTemplate, database, this, tableName); + } + + @Override + protected Type getType(String typeName) { + return new PostgreSQLType(jdbcTemplate, database, this, typeName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLTable.java new file mode 100644 index 00000000..bd0e11ea --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLTable.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * PostgreSQL-specific table. + */ +public class PostgreSQLTable extends Table { + /** + * Creates a new PostgreSQL table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + PostgreSQLTable(JdbcTemplate jdbcTemplate, PostgreSQLDatabase database, PostgreSQLSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE"); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForBoolean("SELECT EXISTS (\n" + + " SELECT 1\n" + + " FROM pg_catalog.pg_class c\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + + " WHERE n.nspname = ?\n" + + " AND c.relname = ?\n" + + " AND c.relkind = 'r'\n" + // only tables + ")", schema.getName(), name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("SELECT * FROM " + this + " FOR UPDATE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLType.java new file mode 100644 index 00000000..e0aad834 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLType.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.postgresql; + +import org.flywaydb.core.internal.database.base.Type; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * PostgreSQL-specific type. + */ +public class PostgreSQLType extends Type { + /** + * Creates a new PostgreSQL type. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this type lives in. + * @param name The name of the type. + */ + public PostgreSQLType(JdbcTemplate jdbcTemplate, PostgreSQLDatabase database, PostgreSQLSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TYPE " + database.quote(schema.getName(), name)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/package-info.java new file mode 100644 index 00000000..8820a741 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.postgresql; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftConnection.java new file mode 100644 index 00000000..24ab27c9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftConnection.java @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; + +/** + * Redshift connection. + */ +public class RedshiftConnection extends Connection { + RedshiftConnection(RedshiftDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SHOW search_path"); + } + + @Override + public void changeCurrentSchemaTo(Schema schema) { + try { + if (schema.getName().equals(originalSchemaNameOrSearchPath) || originalSchemaNameOrSearchPath.startsWith(schema.getName() + ",") || !schema.exists()) { + return; + } + + if (StringUtils.hasText(originalSchemaNameOrSearchPath) && !"unset".equals(originalSchemaNameOrSearchPath)) { + doChangeCurrentSchemaOrSearchPathTo(schema.toString() + "," + originalSchemaNameOrSearchPath); + } else { + doChangeCurrentSchemaOrSearchPathTo(schema.toString()); + } + } catch (SQLException e) { + throw new FlywaySqlException("Error setting current schema to " + schema, e); + } + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + if ("unset".equals(schema)) { + schema = ""; + } + jdbcTemplate.execute("SELECT set_config('search_path', ?, false)", schema); + } + + @Override + public Schema doGetCurrentSchema() throws SQLException { + String currentSchema = jdbcTemplate.queryForString("SELECT current_schema()"); + String searchPath = getCurrentSchemaNameOrSearchPath(); + + if (!StringUtils.hasText(currentSchema) && !StringUtils.hasText(searchPath)) { + throw new FlywayException("Unable to determine current schema as search_path is empty. " + + "Set the current schema in currentSchema parameter of the JDBC URL or in Flyway's schemas property."); + } + + String schema = StringUtils.hasText(currentSchema) ? currentSchema : searchPath; + + return getSchema(schema); + } + + @Override + public Schema getSchema(String name) { + return new RedshiftSchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftDatabase.java new file mode 100644 index 00000000..d4f9e3da --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftDatabase.java @@ -0,0 +1,119 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Redshift database. + */ +public class RedshiftDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public RedshiftDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected RedshiftConnection doGetConnection(Connection connection) { + return new RedshiftConnection(this, connection); + } + + @Override + public final void ensureSupported() { + // Always latest Redshift version. + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL SORTKEY,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INTEGER,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT getdate(),\n" + + " \"execution_time\" INTEGER NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");"; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT current_user"); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "TRUE"; + } + + @Override + public String getBooleanFalse() { + return "FALSE"; + } + + @Override + public String doQuote(String identifier) { + return redshiftQuote(identifier); + } + + static String redshiftQuote(String identifier) { + return "\"" + StringUtils.replaceAll(identifier, "\"", "\"\"") + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftParser.java new file mode 100644 index 00000000..5a957f54 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftParser.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +public class RedshiftParser extends Parser { + private static final Pattern CREATE_LIBRARY_REGEX = Pattern.compile("^(CREATE|DROP) LIBRARY"); + private static final Pattern CREATE_EXTERNAL_TABLE_REGEX = Pattern.compile("^CREATE EXTERNAL TABLE"); + private static final Pattern VACUUM_REGEX = Pattern.compile("^VACUUM"); + private static final Pattern ALTER_TABLE_APPEND_FROM_REGEX = Pattern.compile("^ALTER TABLE( .*)? APPEND FROM"); + private static final Pattern ALTER_TABLE_ALTER_COLUMN_REGEX = Pattern.compile("^ALTER TABLE( .*)? ALTER COLUMN"); + + public RedshiftParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '$'; + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + if (CREATE_LIBRARY_REGEX.matcher(simplifiedStatement).matches() + || CREATE_EXTERNAL_TABLE_REGEX.matcher(simplifiedStatement).matches() + || VACUUM_REGEX.matcher(simplifiedStatement).matches() + || ALTER_TABLE_APPEND_FROM_REGEX.matcher(simplifiedStatement).matches() + || ALTER_TABLE_ALTER_COLUMN_REGEX.matcher(simplifiedStatement).matches()) { + return false; + } + + return null; + } + + @SuppressWarnings("Duplicates") + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + String dollarQuote = (char) reader.read() + reader.readUntilIncluding('$'); + reader.swallowUntilExcluding(dollarQuote); + reader.swallow(dollarQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftSchema.java new file mode 100644 index 00000000..c36b7283 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftSchema.java @@ -0,0 +1,169 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.database.base.Type; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * PostgreSQL implementation of Schema. + */ +public class RedshiftSchema extends Schema { + /** + * Creates a new PostgreSQL schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + RedshiftSchema(JdbcTemplate jdbcTemplate, RedshiftDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM pg_namespace WHERE nspname=?", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + return !jdbcTemplate.queryForBoolean("SELECT EXISTS ( SELECT 1\n" + + " FROM pg_catalog.pg_class c\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + + " WHERE n.nspname = ?)", name); + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " CASCADE"); + } + + @Override + protected void doClean() throws SQLException { + for (String statement : generateDropStatementsForViews()) { + jdbcTemplate.execute(statement); + } + + for (Table table : allTables()) { + table.drop(); + } + + for (String statement : generateDropStatementsForRoutines('a', "FUNCTION", " CASCADE")) { + jdbcTemplate.execute(statement); + } + for (String statement : generateDropStatementsForRoutines('f', "FUNCTION", " CASCADE")) { + jdbcTemplate.execute(statement); + } + for (String statement : generateDropStatementsForRoutines('p', "PROCEDURE", "")) { + jdbcTemplate.execute(statement); + } + } + + /** + * Generates the statements for dropping the routines in this schema. + * + * @kind The kind of object: f for functions, a for aggregate functions, p for procedures + * @objType The type of object for the DROP statement; FUNCTION or PROCEDURE + * @cascade CASCADE if required, blank if not. + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForRoutines(char kind, String objType, String cascade) throws SQLException { + List> rows = + jdbcTemplate.queryForList( + // Search for all functions + "SELECT proname, oidvectortypes(proargtypes) AS args " + + "FROM pg_proc_info INNER JOIN pg_namespace ns ON (pg_proc_info.pronamespace = ns.oid) " + // that don't depend on an extension + + "LEFT JOIN pg_depend dep ON dep.objid = pg_proc_info.prooid AND dep.deptype = 'e' " + + "WHERE pg_proc_info.proisagg = false AND pg_proc_info.prokind = '" + kind + "' " + + "AND ns.nspname = ? AND dep.objid IS NULL", + name + ); + + List statements = new ArrayList<>(); + for (Map row : rows) { + statements.add("DROP " + objType + database.quote(name, row.get("proname")) + "(" + row.get("args") + ") " + cascade); + } + return statements; + } + + /** + * Generates the statements for dropping the views in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List generateDropStatementsForViews() throws SQLException { + List viewNames = + jdbcTemplate.queryForStringList( + // Search for all views + "SELECT relname FROM pg_catalog.pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace" + + // that don't depend on an extension + " LEFT JOIN pg_depend dep ON dep.objid = c.oid AND dep.deptype = 'e'" + + " WHERE c.relkind = 'v' AND n.nspname = ? AND dep.objid IS NULL", + name); + List statements = new ArrayList<>(); + for (String domainName : viewNames) { + statements.add("DROP VIEW IF EXISTS " + database.quote(name, domainName) + " CASCADE"); + } + + return statements; + } + + @Override + protected RedshiftTable[] doAllTables() throws SQLException { + List tableNames = + jdbcTemplate.queryForStringList( + //Search for all the table names + "SELECT t.table_name FROM information_schema.tables t" + + //in this schema + " WHERE table_schema=?" + + //that are real tables (as opposed to views) + " AND table_type='BASE TABLE'", + name + ); + //Views and child tables are excluded as they are dropped with the parent table when using cascade. + + RedshiftTable[] tables = new RedshiftTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new RedshiftTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new RedshiftTable(jdbcTemplate, database, this, tableName); + } + + @Override + protected Type getType(String typeName) { + return new RedshiftType(jdbcTemplate, database, this, typeName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftTable.java new file mode 100644 index 00000000..a14705e6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftTable.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Redshift-specific table. + */ +public class RedshiftTable extends Table { + /** + * Creates a new Redshift table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + RedshiftTable(JdbcTemplate jdbcTemplate, RedshiftDatabase database, RedshiftSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE"); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForBoolean("SELECT EXISTS (\n" + + " SELECT 1\n" + + " FROM pg_catalog.pg_class c\n" + + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + + " WHERE n.nspname = ?\n" + + " AND c.relname = ?\n" + + " AND c.relkind = 'r'\n" + // only tables + ")", schema.getName(), + name.toLowerCase() // Redshift table names are case-insensitive and always in lowercase in pg_class. + ); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("DELETE FROM " + this + " WHERE FALSE"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftType.java new file mode 100644 index 00000000..4fffe469 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftType.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.redshift; + +import org.flywaydb.core.internal.database.base.Type; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * PostgreSQL-specific type. + */ +public class RedshiftType extends Type { + /** + * Creates a new PostgreSQL type. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this type lives in. + * @param name The name of the type. + */ + public RedshiftType(JdbcTemplate jdbcTemplate, RedshiftDatabase database, RedshiftSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TYPE " + database.quote(schema.getName(), name)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/package-info.java new file mode 100644 index 00000000..d0725aa0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.redshift; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAConnection.java new file mode 100644 index 00000000..d8c0b910 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAConnection.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.saphana; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + +public class SAPHANAConnection extends Connection { + SAPHANAConnection(SAPHANADatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT CURRENT_SCHEMA FROM DUMMY"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("SET SCHEMA " + database.doQuote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new SAPHANASchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANADatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANADatabase.java new file mode 100644 index 00000000..4c2cce36 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANADatabase.java @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.saphana; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; + +/** + * SAP HANA database. + */ +public class SAPHANADatabase extends Database { + /** + * Creates a new instance. + */ + public SAPHANADatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected SAPHANAConnection doGetConnection(Connection connection) { + return new SAPHANAConnection(this, connection); + } + + + + + + + + @Override + public void ensureSupported() { + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("2", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessaryForMajorVersion("2"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" TINYINT NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\");\n" + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAParser.java new file mode 100644 index 00000000..0a1e71f5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAParser.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.saphana; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +public class SAPHANAParser extends Parser { + private static final StatementType FUNCTION_OR_PROCEDURE_STATEMENT = new StatementType(); + private static final Pattern FUNCTION_OR_PROCEDURE_REGEX = Pattern.compile( + "^CREATE(\\sOR\\sREPLACE)?\\s(FUNCTION|PROCEDURE)"); + + private static final StatementType ANONYMOUS_BLOCK_STATEMENT = new StatementType(); + private static final Pattern ANONYMOUS_BLOCK_REGEX = Pattern.compile( + "^DO.*BEGIN"); + + public SAPHANAParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 2); + } + + @Override + protected StatementType detectStatementType(String simplifiedStatement) { + if (FUNCTION_OR_PROCEDURE_REGEX.matcher(simplifiedStatement).matches()) { + return FUNCTION_OR_PROCEDURE_STATEMENT; + } + if (ANONYMOUS_BLOCK_REGEX.matcher(simplifiedStatement).matches()) { + return ANONYMOUS_BLOCK_STATEMENT; + } + + return super.detectStatementType(simplifiedStatement); + } + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, Token token) { + TokenType tokenType = token.getType(); + if ((context.getStatementType() == FUNCTION_OR_PROCEDURE_STATEMENT || context.getStatementType() == ANONYMOUS_BLOCK_STATEMENT) && + (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) { + return true; + } + + return super.shouldAdjustBlockDepth(context, token); + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + int parensDepth = keyword.getParensDepth(); + + // BEGIN, CASE, DO and IF increases block depth + if ("BEGIN".equals(keyword.getText()) || "CASE".equals(keyword.getText()) || "DO".equals(keyword.getText()) || "IF".equals(keyword.getText()) + // But not END IF + && !lastTokenIs(tokens, parensDepth, "END")) { + context.increaseBlockDepth(); + } else if (doTokensMatchPattern(tokens, keyword, FUNCTION_OR_PROCEDURE_REGEX)) { + context.increaseBlockDepth(); + } else if ("END".equals(keyword.getText())) { + context.decreaseBlockDepth(); + } + + TokenType tokenType = keyword.getType(); + if ((context.getStatementType() == FUNCTION_OR_PROCEDURE_STATEMENT || context.getStatementType() == ANONYMOUS_BLOCK_STATEMENT) && + (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType) && + context.getBlockDepth() == 1 && + lastTokenIs(tokens, parensDepth, "END")) { + context.decreaseBlockDepth(); + return; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANASchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANASchema.java new file mode 100644 index 00000000..60e81b35 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANASchema.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.saphana; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * SAP HANA implementation of Schema. + */ +public class SAPHANASchema extends Schema { + /** + * Creates a new SAP HANA schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + SAPHANASchema(JdbcTemplate jdbcTemplate, SAPHANADatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM SYS.SCHEMAS WHERE SCHEMA_NAME=?", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + int objectCount = jdbcTemplate.queryForInt("select count(*) from sys.tables where schema_name = ?", name); + objectCount += jdbcTemplate.queryForInt("select count(*) from sys.views where schema_name = ?", name); + objectCount += jdbcTemplate.queryForInt("select count(*) from sys.sequences where schema_name = ?", name); + objectCount += jdbcTemplate.queryForInt("select count(*) from sys.synonyms where schema_name = ?", name); + return objectCount == 0; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + clean(); + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name) + " RESTRICT"); + } + + @Override + protected void doClean() throws SQLException { + for (String dropStatement : generateDropStatements("SYNONYM")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatements("VIEW")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatements("TABLE")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatements("SEQUENCE")) { + jdbcTemplate.execute(dropStatement); + } + } + + /** + * Generates DROP statements for this type of object in this schema. + * + * @param objectType The type of object. + * @return The drop statements. + * @throws SQLException when the statements could not be generated. + */ + private List generateDropStatements(String objectType) throws SQLException { + List dropStatements = new ArrayList<>(); + List dbObjects = getDbObjects(objectType); + for (String dbObject : dbObjects) { + dropStatements.add("DROP " + objectType + " " + database.quote(name, dbObject) + " CASCADE"); + } + return dropStatements; + } + + private List getDbObjects(String objectType) throws SQLException { + return jdbcTemplate.queryForStringList( + "select " + objectType + "_NAME from SYS." + objectType + "S where SCHEMA_NAME = ?", name); + } + + @Override + protected SAPHANATable[] doAllTables() throws SQLException { + List tableNames = getDbObjects("TABLE"); + SAPHANATable[] tables = new SAPHANATable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new SAPHANATable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new SAPHANATable(jdbcTemplate, database, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANATable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANATable.java new file mode 100644 index 00000000..b8dbb70d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANATable.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.saphana; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * SAP HANA-specific table. + */ +public class SAPHANATable extends Table { + /** + * Creates a new SAP HANA table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + SAPHANATable(JdbcTemplate jdbcTemplate, SAPHANADatabase database, SAPHANASchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name)); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.update("lock table " + this + " in exclusive mode"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/package-info.java new file mode 100644 index 00000000..33f53a85 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.saphana; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeConnection.java new file mode 100644 index 00000000..1fea4eac --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeConnection.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.snowflake; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; + + + + + + +public class SnowflakeConnection extends Connection { + + private final String originalRole; + + SnowflakeConnection(SnowflakeDatabase database, java.sql.Connection connection) { + super(database, connection); + try { + this.originalRole = jdbcTemplate.queryForString("SELECT CURRENT_ROLE()"); + } catch (SQLException e) { + throw new FlywayException("Unable to determine current role", e); + } + } + + @Override + protected void doRestoreOriginalState() throws SQLException { + // Reset the role to its original value in case a migration or callback changed it + jdbcTemplate.execute("USE ROLE " + originalRole); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + String schemaName = jdbcTemplate.queryForString("SELECT CURRENT_SCHEMA()"); + return (schemaName != null) ? schemaName : "PUBLIC"; + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("USE SCHEMA " + database.doQuote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new SnowflakeSchema(jdbcTemplate, database, name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java new file mode 100644 index 00000000..12715fb5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java @@ -0,0 +1,181 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.snowflake; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.database.mysql.MySQLDatabase; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +public class SnowflakeDatabase extends Database { + private static final Log LOG = LogFactory.getLog(SnowflakeDatabase.class); + + /** + * Whether quoted identifiers are treated in a case-insensitive way. Defaults to false. See + * https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html#controlling-case-using-the-quoted-identifiers-ignore-case-parameter + */ + private final boolean quotedIdentifiersIgnoreCase; + + /** + * Creates a new instance. + */ + public SnowflakeDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + + quotedIdentifiersIgnoreCase = getQuotedIdentifiersIgnoreCase(jdbcTemplate); + if (quotedIdentifiersIgnoreCase) { + LOG.warn("Current Flyway history table can't be used with QUOTED_IDENTIFIERS_IGNORE_CASE option on"); + } + } + + private static boolean getQuotedIdentifiersIgnoreCase(JdbcTemplate jdbcTemplate) { + try { + // Attempt query + List> result = jdbcTemplate.queryForList("SHOW PARAMETERS LIKE 'QUOTED_IDENTIFIERS_IGNORE_CASE'"); + Map row = result.get(0); + return "TRUE".equals(row.get("value").toUpperCase()); + } catch (SQLException e) { + LOG.warn("Could not query for parameter QUOTED_IDENTIFIERS_IGNORE_CASE."); + return false; + } + } + + @Override + protected SnowflakeConnection doGetConnection(Connection connection) { + return new SnowflakeConnection(this, connection); + } + + + + + + + + + + @Override + public void ensureSupported() { + ensureDatabaseIsRecentEnough("3.0"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("3", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessaryForMajorVersion("4.2"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + // CAUTION: Quotes are optional around column names without underscores; but without them, Snowflake will + // uppercase the column name leading to SELECTs failing. + return "CREATE TABLE " + table + " (\n" + + quote("installed_rank") + " NUMBER(38,0) NOT NULL,\n" + + quote("version") + " VARCHAR(50),\n" + + quote("description") + " VARCHAR(200),\n" + + quote("type") + " VARCHAR(20) NOT NULL,\n" + + quote("script") + " VARCHAR(1000) NOT NULL,\n" + + quote("checksum") + " NUMBER(38,0),\n" + + quote("installed_by") + " VARCHAR(100) NOT NULL,\n" + + quote("installed_on") + " TIMESTAMP_LTZ(9) NOT NULL DEFAULT CURRENT_TIMESTAMP(),\n" + + quote("execution_time") + " NUMBER(38,0) NOT NULL,\n" + + quote("success") + " BOOLEAN NOT NULL,\n" + + "primary key (" + quote("installed_rank") + "));\n" + + + (baseline ? getBaselineStatement(table) + ";\n" : ""); + } + + @Override + public String getSelectStatement(Table table) { + // CAUTION: Quotes are optional around column names without underscores; but without them, Snowflake will + // uppercase the column name. In data readers, the column name is case sensitive. + return "SELECT " + quote("installed_rank") + + "," + quote("version") + + "," + quote("description") + + "," + quote("type") + + "," + quote("script") + + "," + quote("checksum") + + "," + quote("installed_on") + + "," + quote("installed_by") + + "," + quote("execution_time") + + "," + quote("success") + + " FROM " + table + + " WHERE " + quote("installed_rank") + " > ?" + + " ORDER BY " + quote("installed_rank"); + } + + @Override + public String getInsertStatement(Table table) { + // CAUTION: Quotes are optional around column names without underscores; but without them, Snowflake will + // uppercase the column name. + return "INSERT INTO " + table + + " (" + quote("installed_rank") + + ", " + quote("version") + + ", " + quote("description") + + ", " + quote("type") + + ", " + quote("script") + + ", " + quote("checksum") + + ", " + quote("installed_by") + + ", " + quote("execution_time") + + ", " + quote("success") + + ")" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "true"; + } + + @Override + public String getBooleanFalse() { + return "false"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.java new file mode 100644 index 00000000..7266860f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.snowflake; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; + +public class SnowflakeParser extends Parser { + private final String ALTERNATIVE_QUOTE = "$$"; + + public SnowflakeParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 2); + } + + @Override + protected boolean isAlternativeStringLiteral(String peek) { + if (peek.startsWith("$$")) { + return true; + } + + return super.isAlternativeStringLiteral(peek); + } + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(ALTERNATIVE_QUOTE.length()); + reader.swallowUntilExcluding(ALTERNATIVE_QUOTE); + reader.swallow(ALTERNATIVE_QUOTE.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeSchema.java new file mode 100644 index 00000000..05530c47 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeSchema.java @@ -0,0 +1,140 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.snowflake; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class SnowflakeSchema extends Schema { + /** + * Creates a new Snowflake schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + SnowflakeSchema(JdbcTemplate jdbcTemplate, SnowflakeDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + String sql = "SHOW SCHEMAS LIKE '" + name + "'"; + List results = jdbcTemplate.query(sql, new RowMapper() { + @Override + public Boolean mapRow(ResultSet rs) throws SQLException { + return true; + } + }); + return !results.isEmpty(); + } + + @Override + protected boolean doEmpty() throws SQLException { + int objectCount = getObjectCount("TABLE") + getObjectCount("VIEW") + + getObjectCount("SEQUENCE"); + + return objectCount == 0; + } + + private int getObjectCount(String objectType) throws SQLException { + return jdbcTemplate.query("SHOW " + objectType + "S IN SCHEMA " + database.quote(name), new RowMapper() { + @Override + public Integer mapRow(ResultSet rs) throws SQLException { + return 1; + } + }).size(); + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name)); + } + + @Override + protected void doClean() throws SQLException { + for (String dropStatement : generateDropStatements("VIEW")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatements("TABLE")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatements("SEQUENCE")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatementsWithArgs("USER FUNCTIONS", "FUNCTION")) { + jdbcTemplate.execute(dropStatement); + } + + for (String dropStatement : generateDropStatementsWithArgs("PROCEDURES", "PROCEDURE")) { + jdbcTemplate.execute(dropStatement); + } + } + + @Override + protected SnowflakeTable[] doAllTables() throws SQLException { + List tables = jdbcTemplate.query("SHOW TABLES IN SCHEMA " + database.quote(name), new RowMapper() { + @Override + public SnowflakeTable mapRow(ResultSet rs) throws SQLException { + String tableName = rs.getString("name"); + return (SnowflakeTable)getTable(tableName); + } + }); + return tables.toArray(new SnowflakeTable[0]); + } + + @Override + public Table getTable(String tableName) { + return new SnowflakeTable(jdbcTemplate, database, this, tableName); + } + + + private List generateDropStatements(final String objectType) throws SQLException { + return jdbcTemplate.query("SHOW " + objectType + "S IN SCHEMA " + database.quote(name), new RowMapper() { + @Override + public String mapRow(ResultSet rs) throws SQLException { + String tableName = rs.getString("name"); + return "DROP " + objectType + " " + database.quote(name) + "." + database.quote(tableName); + } + }); + } + + private List generateDropStatementsWithArgs(final String showObjectType, final String dropObjectType) throws SQLException { + return jdbcTemplate.query("SHOW " + showObjectType + " IN SCHEMA " + database.quote(name), new RowMapper() { + @Override + public String mapRow(ResultSet rs) throws SQLException { + String nameAndArgsList = rs.getString("arguments"); + int indexOfEndOfArgs = nameAndArgsList.indexOf(") RETURN "); + String functionName = nameAndArgsList.substring(0, indexOfEndOfArgs + 1); + return "DROP " + dropObjectType + " " + name + "." + functionName; + } + }); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeTable.java new file mode 100644 index 00000000..c5857b71 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeTable.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.snowflake; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class SnowflakeTable extends Table { + SnowflakeTable(JdbcTemplate jdbcTemplate, SnowflakeDatabase database, SnowflakeSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName()) + "." + database.quote(name)); + } + + @Override + protected boolean doExists() throws SQLException { + if (!schema.exists()) return false; + + String sql = "SHOW TABLES LIKE '" + name + "' IN SCHEMA " + database.quote(schema.getName()); + List results = jdbcTemplate.query(sql, new RowMapper() { + @Override + public Boolean mapRow(ResultSet rs) throws SQLException { + return true; + } + }); + return !results.isEmpty(); + } + + @Override + protected void doLock() throws SQLException { + // no-op + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/package-info.java new file mode 100644 index 00000000..7737c738 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.snowflake; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteConnection.java new file mode 100644 index 00000000..fe16867c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteConnection.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlite; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +/** + * SQLite connection. + */ +public class SQLiteConnection extends Connection { + SQLiteConnection(SQLiteDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public Schema getSchema(String name) { + return new SQLiteSchema(jdbcTemplate, database, name); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() { + return "main"; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteDatabase.java new file mode 100644 index 00000000..b10cc441 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteDatabase.java @@ -0,0 +1,127 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlite; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; + +import java.sql.Connection; + +/** + * SQLite database. + */ +public class SQLiteDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public SQLiteDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected SQLiteConnection doGetConnection(Connection connection) { + return new SQLiteConnection(this, connection); + } + + @Override + public final void ensureSupported() { + // The minimum should really be 3.7.2. However the SQLite driver quality is really hit and miss, so we can't + // reliably detect this. + // #2221: Older versions of the Xerial JDBC driver misreport 3.x versions as being 3.0. + // #2409: SQLDroid misreports the version as 0.0 + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " \"installed_rank\" INT NOT NULL PRIMARY KEY,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INT,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),\n" + + " \"execution_time\" INT NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON \"" + table.getName() + "\" (\"success\");"; + } + + public String getDbName() { + return "sqlite"; + } + + @Override + protected String doGetCurrentUser() { + return ""; + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return false; + } + + + + + + + + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public String doQuote(String identifier) { + return "\"" + identifier + "\""; + } + + @Override + public boolean catalogIsSchema() { + return true; + } + + @Override + public boolean useSingleConnection() { + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteParser.java new file mode 100644 index 00000000..e6ce63ea --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteParser.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlite; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; + +public class SQLiteParser extends Parser { + public SQLiteParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected char getAlternativeIdentifierQuote() { + return '`'; + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + if ("PRAGMA FOREIGN_KEYS".equals(simplifiedStatement)) { + return false; + } + + return null; + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + String lastKeyword = keyword.getText(); + if ("BEGIN".equals(lastKeyword) || "CASE".equals(lastKeyword)) { + context.increaseBlockDepth(); + } else if ("END".equals(lastKeyword)) { + context.decreaseBlockDepth(); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteSchema.java new file mode 100644 index 00000000..11ad03b5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteSchema.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlite; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * SQLite implementation of Schema. + */ +public class SQLiteSchema extends Schema { + private static final Log LOG = LogFactory.getLog(SQLiteSchema.class); + + private static final List IGNORED_SYSTEM_TABLE_NAMES = + Arrays.asList("android_metadata", SQLiteTable.SQLITE_SEQUENCE); + + private boolean foreignKeysEnabled; + + /** + * Creates a new SQLite schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + SQLiteSchema(JdbcTemplate jdbcTemplate, SQLiteDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + try { + doAllTables(); + return true; + } catch (SQLException e) { + return false; + } + } + + @Override + protected boolean doEmpty() { + Table[] tables = allTables(); + List tableNames = new ArrayList<>(); + for (Table table : tables) { + String tableName = table.getName(); + if (!IGNORED_SYSTEM_TABLE_NAMES.contains(tableName)) { + tableNames.add(tableName); + } + } + return tableNames.isEmpty(); + } + + @Override + protected void doCreate() { + LOG.info("SQLite does not support creating schemas. Schema not created: " + name); + } + + @Override + protected void doDrop() { + LOG.info("SQLite does not support dropping schemas. Schema not dropped: " + name); + } + + @Override + protected void doClean() throws SQLException { + foreignKeysEnabled = jdbcTemplate.queryForBoolean("PRAGMA foreign_keys"); + + List viewNames = jdbcTemplate.queryForStringList("SELECT tbl_name FROM " + database.quote(name) + ".sqlite_master WHERE type='view'"); + + for (String viewName : viewNames) { + jdbcTemplate.execute("DROP VIEW " + database.quote(name, viewName)); + } + + for (Table table : allTables()) { + table.drop(); + } + + if (getTable(SQLiteTable.SQLITE_SEQUENCE).exists()) { + jdbcTemplate.execute("DELETE FROM " + SQLiteTable.SQLITE_SEQUENCE); + } + } + + @Override + protected SQLiteTable[] doAllTables() throws SQLException { + List tableNames = jdbcTemplate.queryForStringList("SELECT tbl_name FROM " + database.quote(name) + ".sqlite_master WHERE type='table'"); + + SQLiteTable[] tables = new SQLiteTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new SQLiteTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new SQLiteTable(jdbcTemplate, database, this, tableName); + } + + public boolean getForeignKeysEnabled() { return foreignKeysEnabled; } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteTable.java new file mode 100644 index 00000000..49edf742 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteTable.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlite; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * SQLite-specific table. + */ +public class SQLiteTable extends Table { + private static final Log LOG = LogFactory.getLog(SQLiteTable.class); + + /** + * SQLite system tables are undroppable. + */ + static final String SQLITE_SEQUENCE = "sqlite_sequence"; + private final boolean undroppable; + + /** + * Creates a new SQLite table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public SQLiteTable(JdbcTemplate jdbcTemplate, SQLiteDatabase database, SQLiteSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + undroppable = SQLITE_SEQUENCE.equals(name); + } + + @Override + protected void doDrop() throws SQLException { + if (undroppable) { + LOG.debug("SQLite system table " + this + " cannot be dropped. Ignoring."); + } else { + String dropSql = "DROP TABLE " + database.quote(schema.getName(), name); + if (getSchema().getForeignKeysEnabled()) { + // #2417: Disable foreign keys before dropping tables to avoid constraint violation errors + dropSql = "PRAGMA foreign_keys = OFF; " + dropSql + "; PRAGMA foreign_keys = ON"; + } + jdbcTemplate.execute(dropSql); + } + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT count(tbl_name) FROM " + + database.quote(schema.getName()) + ".sqlite_master WHERE type='table' AND tbl_name='" + name + "'") > 0; + } + + @Override + protected void doLock() { + LOG.debug("Unable to lock " + this + " as SQLite does not support locking. No concurrent migration supported."); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/package-info.java new file mode 100644 index 00000000..f97a76f8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.sqlite; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerApplicationLockTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerApplicationLockTemplate.java new file mode 100644 index 00000000..fe9c9d93 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerApplicationLockTemplate.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing with SQL Server application locks. + */ +public class SQLServerApplicationLockTemplate { + private static final Log LOG = LogFactory.getLog(SQLServerApplicationLockTemplate.class); + + private final SQLServerConnection connection; + private final JdbcTemplate jdbcTemplate; + private final String databaseName; + private final String lockName; + + /** + * Creates a new application lock template for this connection. + * @param connection The connection reference. + * @param jdbcTemplate The jdbcTemplate for the connection. + * @param discriminator A number to discriminate between locks. + */ + SQLServerApplicationLockTemplate(SQLServerConnection connection, JdbcTemplate jdbcTemplate, String databaseName, int discriminator) { + this.connection = connection; + this.jdbcTemplate = jdbcTemplate; + this.databaseName = databaseName; + lockName = "Flyway-" + discriminator; + } + + /** + * Executes this callback with an advisory lock. + * + * @param callable The callback to execute. + * @return The result of the callable code. + */ + public T execute(Callable callable) { + try { + connection.setCurrentDatabase(databaseName); + jdbcTemplate.execute("EXEC sp_getapplock @Resource = ?, @LockTimeout='3600000'," + + " @LockMode = 'Exclusive', @LockOwner = 'Session'", lockName); + return callable.call(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to acquire SQL Server application lock", e); + } catch (Exception e) { + RuntimeException rethrow; + if (e instanceof RuntimeException) { + rethrow = (RuntimeException) e; + } else { + rethrow = new FlywayException(e); + } + throw rethrow; + } finally { + try { + connection.setCurrentDatabase(databaseName); + jdbcTemplate.execute("EXEC sp_releaseapplock @Resource = ?, @LockOwner = 'Session'", lockName); + } catch (SQLException e) { + LOG.error("Unable to release SQL Server application lock", e); + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerConnection.java new file mode 100644 index 00000000..01d2bd93 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerConnection.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; + +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * SQL Server connection. + */ +public class SQLServerConnection extends Connection { + private final String originalDatabaseName; + private final String originalAnsiNulls; + private final boolean azure; + private final SQLServerEngineEdition engineEdition; + + SQLServerConnection(SQLServerDatabase database, java.sql.Connection connection) { + super(database, connection); + try { + originalDatabaseName = jdbcTemplate.queryForString("SELECT DB_NAME()"); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine current database", e); + } + + try { + azure = "SQL Azure".equals(getJdbcTemplate().queryForString( + "SELECT CAST(SERVERPROPERTY('edition') AS VARCHAR)")); + } + catch (SQLException e) { + throw new FlywaySqlException("Unable to determine database edition.'", e); + } + + try { + engineEdition = SQLServerEngineEdition.fromCode(getJdbcTemplate().queryForInt( + "SELECT SERVERPROPERTY('engineedition')")); + } + catch (SQLException e) { + throw new FlywaySqlException("Unable to determine database engine edition.'", e); + } + + try { + originalAnsiNulls = azure ? null : + jdbcTemplate.queryForString("DECLARE @ANSI_NULLS VARCHAR(3) = 'OFF';\n" + + "IF ( (32 & @@OPTIONS) = 32 ) SET @ANSI_NULLS = 'ON';\n" + + "SELECT @ANSI_NULLS AS ANSI_NULLS;"); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to determine ANSI NULLS state", e); + } + } + + void setCurrentDatabase(String databaseName) throws SQLException { + if (!azure) { + jdbcTemplate.execute("USE " + database.quote(databaseName)); + } + } + + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT SCHEMA_NAME()"); + } + + @Override + protected void doRestoreOriginalState() throws SQLException { + setCurrentDatabase(originalDatabaseName); + if (!azure) { + jdbcTemplate.execute("SET ANSI_NULLS " + originalAnsiNulls); + } + } + + @Override + public Schema getSchema(String name) { + return new SQLServerSchema(jdbcTemplate, database, originalDatabaseName, name); + } + + @Override + public T lock(Table table, Callable callable) { + return new SQLServerApplicationLockTemplate(this, jdbcTemplate, originalDatabaseName, table.toString().hashCode()).execute(callable); + } + + public Boolean isAzureConnection() { return azure; } + + public SQLServerEngineEdition getEngineEdition() { return engineEdition; } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerDatabase.java new file mode 100644 index 00000000..d5af4974 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerDatabase.java @@ -0,0 +1,296 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * SQL Server database. + */ +public class SQLServerDatabase extends Database { + /** + * Creates a new instance. + * + * @param configuration The Flyway configuration. + */ + public SQLServerDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected SQLServerConnection doGetConnection(Connection connection) { + return new SQLServerConnection(this, connection); + } + + + + + + + + + + + + + + + + + + + + @Override + public final void ensureSupported() { + if (isAzure()) { + ensureDatabaseIsRecentEnough("11.0"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("12.0", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("12.0"); + } else { + ensureDatabaseIsRecentEnough("10.0"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("13.0", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("15.0"); + } + } + + @Override + protected String computeVersionDisplayName(MigrationVersion version) { + if (isAzure()) { + return "Azure v" + getVersion().getMajorAsString(); + } + + if (getVersion().isAtLeast("8")) { + if ("8".equals(getVersion().getMajorAsString())) { + return "2000"; + } + if ("9".equals(getVersion().getMajorAsString())) { + return "2005"; + } + if ("10".equals(getVersion().getMajorAsString())) { + if ("0".equals(getVersion().getMinorAsString())) { + return "2008"; + } + return "2008 R2"; + } + if ("11".equals(getVersion().getMajorAsString())) { + return "2012"; + } + if ("12".equals(getVersion().getMajorAsString())) { + return "2014"; + } + if ("13".equals(getVersion().getMajorAsString())) { + return "2016"; + } + if ("14".equals(getVersion().getMajorAsString())) { + return "2017"; + } + if ("15".equals(getVersion().getMajorAsString())) { + return "2019"; + } + } + return super.computeVersionDisplayName(version); + } + + @Override + public Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT SUSER_SNAME()"); + } + + @Override + public boolean supportsDdlTransactions() { + return true; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return false; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + /** + * Escapes this identifier, so it can be safely used in sql queries. + * + * @param identifier The identifier to escaped. + * @return The escaped version. + */ + private String escapeIdentifier(String identifier) { + return StringUtils.replaceAll(identifier, "]", "]]"); + } + + @Override + public String doQuote(String identifier) { + return "[" + escapeIdentifier(identifier) + "]"; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + public boolean useSingleConnection() { + return true; + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String filegroup = isAzure() || configuration.getTablespace() == null + ? "" + : " ON \"" + configuration.getTablespace() + "\""; + + return "CREATE TABLE " + table + " (\n" + + " [installed_rank] INT NOT NULL,\n" + + " [" + "version] NVARCHAR(50),\n" + + " [description] NVARCHAR(200),\n" + + " [type] NVARCHAR(20) NOT NULL,\n" + + " [script] NVARCHAR(1000) NOT NULL,\n" + + " [checksum] INT,\n" + + " [installed_by] NVARCHAR(100) NOT NULL,\n" + + " [installed_on] DATETIME NOT NULL DEFAULT GETDATE(),\n" + + " [execution_time] INT NOT NULL,\n" + + " [success] BIT NOT NULL\n" + + ")" + filegroup + ";\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "ALTER TABLE " + table + " ADD CONSTRAINT [" + table.getName() + "_pk] PRIMARY KEY ([installed_rank]);\n" + + "CREATE INDEX [" + table.getName() + "_s_idx] ON " + table + " ([success]);\n" + + "GO\n"; + } + + /** + * @return Whether this is a SQL Azure database. + */ + boolean isAzure() { + return getMainConnection().isAzureConnection(); + } + + /** + * @return The database engine edition. + */ + SQLServerEngineEdition getEngineEdition() { + return getMainConnection().getEngineEdition(); + } + + /** + * @return Whether this database supports temporal tables + */ + boolean supportsTemporalTables() { + // SQL Server 2016+, or Azure (which has different versioning) + return isAzure() || getVersion().isAtLeast("13.0"); + } + + /** + * @return Whether this database supports partitions + */ + boolean supportsPartitions() { + return isAzure() + || SQLServerEngineEdition.ENTERPRISE.equals(getEngineEdition()) + || getVersion().isAtLeast("13"); + } + + /** + * @return Whether this database supports sequences + */ + boolean supportsSequences() { + return getVersion().isAtLeast("11"); + } + + /** + * Cleans all the objects in this database that need to be done after cleaning schemas. + * + * @throws SQLException when the clean failed. + */ + @Override + protected void doCleanPostSchemas() throws SQLException { + if (supportsPartitions()) { + for (String statement : cleanPartitionSchemes()) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanPartitionFunctions()) { + jdbcTemplate.execute(statement); + } + } + } + + /** + * Cleans the Partition Schemes in this database. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanPartitionSchemes() throws SQLException { + List partitionSchemeNames = + jdbcTemplate.queryForStringList("SELECT name FROM sys.partition_schemes"); + List statements = new ArrayList<>(); + for (String partitionSchemeName : partitionSchemeNames) { + statements.add("DROP PARTITION SCHEME " + quote(partitionSchemeName)); + } + return statements; + } + + /** + * Cleans the Partition Functions in this database. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanPartitionFunctions() throws SQLException { + List partitionFunctionNames = + jdbcTemplate.queryForStringList("SELECT name FROM sys.partition_functions"); + List statements = new ArrayList<>(); + for (String partitionFunctionName : partitionFunctionNames) { + statements.add("DROP PARTITION FUNCTION " + quote(partitionFunctionName)); + } + return statements; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerEngineEdition.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerEngineEdition.java new file mode 100644 index 00000000..45d55b01 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerEngineEdition.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +/* + * SQL Server engine editions. Some restrict the functionality available. See + * https://docs.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver15 + * for details of what each edition supports. + */ +public enum SQLServerEngineEdition { + + PERSONAL_DESKTOP(1), + STANDARD(2), + ENTERPRISE(3), + EXPRESS(4), + SQL_DATABASE(5), + SQL_DATA_WAREHOUSE(6), + MANAGED_INSTANCE(8); + + private final int code; + + SQLServerEngineEdition(int code) { + this.code = code; + } + + public static SQLServerEngineEdition fromCode(int code) { + for (SQLServerEngineEdition edition : values()) { + if (edition.code == code) { + return edition; + } + } + throw new IllegalArgumentException("Unknown SQL Server engine edition: " + code); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerParser.java new file mode 100644 index 00000000..77bd9dfb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerParser.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; +import org.flywaydb.core.internal.sqlscript.Delimiter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class SQLServerParser extends Parser { + // #2175, 2298, 2542: Various system sprocs, mostly around replication, cannot be executed within a transaction. + // These procedures are only present in SQL Server. Not on Azure nor in PDW. + private static final List SPROCS_INVALID_IN_TRANSACTIONS = Arrays.asList( + "SP_ADDSUBSCRIPTION", "SP_DROPSUBSCRIPTION", + "SP_ADDDISTRIBUTOR", "SP_DROPDISTRIBUTOR", + "SP_ADDDISTPUBLISHER", "SP_DROPDISTPUBLISHER", + "SP_ADDLINKEDSERVER", "SP_DROPLINKEDSERVER", + "SP_ADDLINKEDSRVLOGIN", "SP_DROPLINKEDSRVLOGIN", + "SP_SERVEROPTION", "SP_REPLICATIONDBOPTION"); + + public SQLServerParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + protected boolean isDelimiter(String peek, ParserContext context, int col) { + return peek.length() >= 2 + && (peek.charAt(0) == 'G' || peek.charAt(0) == 'g') + && (peek.charAt(1) == 'O' || peek.charAt(1) == 'o') + && (peek.length() == 2 || Character.isWhitespace(peek.charAt(2))); + } + + @Override + protected String readKeyword(PeekingReader reader, Delimiter delimiter, ParserContext context) throws IOException { + // #2414: Ignore delimiter as GO (unlike ;) can be part of a regular keyword + return "" + (char) reader.read() + reader.readKeywordPart(null, context); + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + String current = keywords.get(keywords.size() - 1).getText(); + if ("BACKUP".equals(current) || "RESTORE".equals(current) || "RECONFIGURE".equals(current)) { + return false; + } + + if (keywords.size() < 2) { + return null; + } + + String previous = keywords.get(keywords.size() - 2).getText(); + + if ("EXEC".equals(previous) && SPROCS_INVALID_IN_TRANSACTIONS.contains(current)) { + return false; + } + + // (CREATE|DROP|ALTER) (DATABASE|FULLTEXT (INDEX|CATALOG)) + if (("CREATE".equals(previous) || "ALTER".equals(previous) || "DROP".equals(previous)) + && ("DATABASE".equals(current) || "FULLTEXT".equals(current))) { + return false; + } + + return null; + } + + @Override + protected int getTransactionalDetectionCutoff() { + return Integer.MAX_VALUE; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerSchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerSchema.java new file mode 100644 index 00000000..5e34863d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerSchema.java @@ -0,0 +1,579 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * SQLServer implementation of Schema. + */ +public class SQLServerSchema extends Schema { + private static final Log LOG = LogFactory.getLog(SQLServerSchema.class); + + private final String databaseName; + + /** + * SQL server object types for which we support automatic clean-up. Those types can be used in conjunction with the + * {@code sys.objects} catalog. The full list of object types is available in the + * MSDN documentation (see the {@code type} + * column description. + */ + private enum ObjectType { + /** + * Aggregate function (CLR). + */ + AGGREGATE("AF"), + /** + * CHECK constraint + */ + CHECK_CONSTRAINT("C"), + /** + * DEFAULT constraint. + */ + DEFAULT_CONSTRAINT("D"), + /** + * FOREIGN KEY constraint. + */ + FOREIGN_KEY("F"), + /** + * In-lined table-function. + */ + INLINED_TABLE_FUNCTION("IF"), + /** + * Scalar function. + */ + SCALAR_FUNCTION("FN"), + /** + * Assembly (CLR) scalar-function. + */ + CLR_SCALAR_FUNCTION("FS"), + /** + * Assembly (CLR) table-valued function + */ + CLR_TABLE_VALUED_FUNCTION("FT"), + /** + * Stored procedure. + */ + STORED_PROCEDURE("P"), + /** + * Assembly (CLR) stored-procedure. + */ + CLR_STORED_PROCEDURE("PC"), + /** + * Rule (old-style, stand-alone). + */ + RULE("R"), + /** + * Synonym. + */ + SYNONYM("SN"), + /** + * Table-valued function. + */ + TABLE_VALUED_FUNCTION("TF"), + /** + * Assembly (CLR) DML trigger. + */ + ASSEMBLY_DML_TRIGGER("TA"), + /** + * SQL DML trigger. + */ + SQL_DML_TRIGGER("TR"), + /** + * Unique Constraint. + */ + UNIQUE_CONSTRAINT("UQ"), + /** + * User table. + */ + USER_TABLE("U"), + /** + * View. + */ + VIEW("V"), + /** + * Sequence object. + */ + SEQUENCE_OBJECT("SO"); + + final String code; + + ObjectType(String code) { + assert code != null; + this.code = code; + } + } + + /** + * SQL server object meta-data. + */ + private class DBObject { + /** + * The object name. + */ + final String name; + /** + * The object id. + */ + final long objectId; + + DBObject(long objectId, String name) { + assert name != null; + this.objectId = objectId; + this.name = name; + } + } + + /** + * Creates a new SQLServer schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param databaseName The database name. + * @param name The name of the schema. + */ + SQLServerSchema(JdbcTemplate jdbcTemplate, SQLServerDatabase database, String databaseName, String name) { + super(jdbcTemplate, database, name); + this.databaseName = databaseName; + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=?", name) > 0; + } + + @Override + protected boolean doEmpty() throws SQLException { + boolean empty = queryDBObjects(ObjectType.SCALAR_FUNCTION, ObjectType.AGGREGATE, + ObjectType.CLR_SCALAR_FUNCTION, ObjectType.CLR_TABLE_VALUED_FUNCTION, ObjectType.TABLE_VALUED_FUNCTION, + ObjectType.STORED_PROCEDURE, ObjectType.CLR_STORED_PROCEDURE, ObjectType.USER_TABLE, + ObjectType.SYNONYM, ObjectType.SEQUENCE_OBJECT, ObjectType.FOREIGN_KEY, ObjectType.VIEW).isEmpty(); + if (empty) { + int objectCount = jdbcTemplate.queryForInt("SELECT count(*) FROM " + + "( " + + "SELECT t.name FROM sys.types t INNER JOIN sys.schemas s ON t.schema_id = s.schema_id" + + " WHERE t.is_user_defined = 1 AND s.name = ? " + + "Union " + + "SELECT name FROM sys.assemblies WHERE is_user_defined=1" + + ") R", name); + empty = objectCount == 0; + } + + return empty; + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE SCHEMA " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + clean(); + jdbcTemplate.execute("DROP SCHEMA " + database.quote(name)); + } + + @Override + protected void doClean() throws SQLException { + List tables = queryDBObjects(ObjectType.USER_TABLE); + + for (String statement : cleanTriggers()) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanForeignKeys(tables)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanDefaultConstraints(tables)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanUniqueConstraints(tables)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanIndexes(tables)) { + jdbcTemplate.execute(statement); + } + + // Use a 2-pass approach for cleaning computed columns and functions with SCHEMABINDING due to dependency errors + // Pass 1 + for (String statement : cleanComputedColumns(tables)) { + try { + jdbcTemplate.execute(statement); + } catch (SQLException e) { + LOG.debug("Ignoring dependency-related error: " + e.getMessage()); + } + } + for (String statement : cleanObjects("FUNCTION", + ObjectType.SCALAR_FUNCTION, + ObjectType.CLR_SCALAR_FUNCTION, + ObjectType.CLR_TABLE_VALUED_FUNCTION, + ObjectType.TABLE_VALUED_FUNCTION, + ObjectType.INLINED_TABLE_FUNCTION)) { + try { + jdbcTemplate.execute(statement); + } catch (SQLException e) { + LOG.debug("Ignoring dependency-related error: " + e.getMessage()); + } + } + + // Pass 2 + for (String statement : cleanComputedColumns(tables)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("PROCEDURE", + ObjectType.STORED_PROCEDURE, + ObjectType.CLR_STORED_PROCEDURE)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("VIEW", ObjectType.VIEW)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("FUNCTION", + ObjectType.SCALAR_FUNCTION, + ObjectType.CLR_SCALAR_FUNCTION, + ObjectType.CLR_TABLE_VALUED_FUNCTION, + ObjectType.TABLE_VALUED_FUNCTION, + ObjectType.INLINED_TABLE_FUNCTION)) { + jdbcTemplate.execute(statement); + } + + SQLServerTable[] allTables = allTables(); + for (SQLServerTable table : allTables) { + table.dropSystemVersioningIfPresent(); + } + for (SQLServerTable table : allTables) { + table.drop(); + } + + for (String statement : cleanObjects("AGGREGATE", ObjectType.AGGREGATE)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanTypes()) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanAssemblies()) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("SYNONYM", ObjectType.SYNONYM)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("RULE", ObjectType.RULE)) { + jdbcTemplate.execute(statement); + } + + for (String statement : cleanObjects("DEFAULT", ObjectType.DEFAULT_CONSTRAINT)) { + jdbcTemplate.execute(statement); + } + + + + + for (String statement : cleanObjects("SEQUENCE", ObjectType.SEQUENCE_OBJECT)) { + jdbcTemplate.execute(statement); + } + + + + } + + /** + * Query objects with any of the given types. + * + * @param types the object types to be queried + * @return the found objects + * @throws SQLException when the retrieval failed + */ + private List queryDBObjects(ObjectType... types) throws SQLException { + return queryDBObjectsWithParent(null, types); + } + + /** + * Query objects with any of the given types and parent (if non-null). + * + * @param parent the parent object or {@code null} if unspecified + * @param types the object types to be queried + * @return the found objects + * @throws SQLException when the retrieval failed + */ + private List queryDBObjectsWithParent(DBObject parent, ObjectType... types) throws SQLException { + StringBuilder query = new StringBuilder("SELECT obj.object_id, obj.name FROM sys.objects AS obj " + + "LEFT JOIN sys.extended_properties AS eps " + + "ON obj.object_id = eps.major_id " + + "AND eps.class = 1 " + // Class 1 = objects and columns (we are only interested in objects). + "AND eps.minor_id = 0 " + // Minor ID, always 0 for objects. + "AND eps.name='microsoft_database_tools_support' " + // Select all objects generated from MS database + // tools. + "WHERE SCHEMA_NAME(obj.schema_id) = '" + name + "' " + + "AND eps.major_id IS NULL " + // Left Excluding JOIN (we are only interested in user defined entries). + "AND obj.is_ms_shipped = 0 " + // Make sure we do not return anything MS shipped. + "AND obj.type IN (" // Select the object types. + ); + + // Build the types IN clause. + boolean first = true; + for (ObjectType type : types) { + if (!first) { + query.append(", "); + } + query.append("'").append(type.code).append("'"); + first = false; + } + query.append(")"); + + if (parent != null) { + // Apply the parent selection if one was given. + query.append(" AND obj.parent_object_id = ").append(parent.objectId); + } + + query.append(" order by create_date desc" + + + + + ); + + return jdbcTemplate.query(query.toString(), new RowMapper() { + @Override + public DBObject mapRow(ResultSet rs) throws SQLException { + return new DBObject(rs.getLong("object_id"), rs.getString("name")); + } + }); + } + + /** + * Cleans the foreign keys in this schema. + * + * @param tables the tables to be cleaned + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanForeignKeys(List tables) throws SQLException { + List statements = new ArrayList<>(); + for (DBObject table : tables) { + List fks = queryDBObjectsWithParent(table, ObjectType.FOREIGN_KEY, + ObjectType.CHECK_CONSTRAINT); + for (DBObject fk : fks) { + statements.add("ALTER TABLE " + database.quote(name, table.name) + " DROP CONSTRAINT " + + database.quote(fk.name)); + } + } + return statements; + } + + /** + * Cleans the computed columns in this schema. + * + * @param tables the tables to be cleaned + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanComputedColumns(List tables) throws SQLException { + List statements = new ArrayList<>(); + for (DBObject table : tables) { + String tableName = database.quote(name, table.name); + List columns = jdbcTemplate.queryForStringList( + "SELECT name FROM sys.computed_columns WHERE object_id=OBJECT_ID(N'" + tableName + "')"); + for (String column : columns) { + statements.add("ALTER TABLE " + tableName + " DROP COLUMN " + database.quote(column)); + } + } + return statements; + } + + /** + * Cleans the indexes in this schema. + * + * @param tables the tables to be cleaned + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanIndexes(List tables) throws SQLException { + List statements = new ArrayList<>(); + for (DBObject table : tables) { + String tableName = database.quote(name, table.name); + List indexes = jdbcTemplate.queryForStringList( + "SELECT name FROM sys.indexes" + + " WHERE object_id=OBJECT_ID(N'" + tableName + "')" + + " AND is_primary_key = 0" + + " AND is_unique_constraint = 0" + + " AND name IS NOT NULL"); + for (String index : indexes) { + statements.add("DROP INDEX " + database.quote(index) + " ON " + tableName); + } + } + return statements; + } + + /** + * Cleans the default constraints in this schema. + * + * @param tables the tables to be cleaned + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanDefaultConstraints(List tables) throws SQLException { + List statements = new ArrayList<>(); + for (DBObject table : tables) { + String tableName = database.quote(name, table.name); + List indexes = jdbcTemplate.queryForStringList( + "SELECT i.name FROM sys.indexes i" + + " JOIN sys.index_columns ic on i.index_id = ic.index_id" + + " JOIN sys.columns c ON ic.column_id = c.column_id AND i.object_id = c.object_id" + + " WHERE i.object_id=OBJECT_ID(N'" + tableName + "')" + + " AND is_primary_key = 0" + + " AND is_unique_constraint = 1" + + " AND i.name IS NOT NULL" + + " GROUP BY i.name" + + // We can't delete the unique ROWGUIDCOL constraint from a table which has a FILESTREAM column. + // It will auto-delete when the table is dropped. + " HAVING MAX(CAST(is_rowguidcol AS INT)) = 0 OR MAX(CAST(is_filestream AS INT)) = 0"); + for (String index : indexes) { + statements.add("ALTER TABLE " + tableName + " DROP CONSTRAINT " + database.quote(index)); + } + } + return statements; + } + + /** + * Cleans the unique constraints in this schema. + * + * @param tables the tables to be cleaned + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanUniqueConstraints(List tables) throws SQLException { + List statements = new ArrayList<>(); + for (DBObject table : tables) { + List dfs = queryDBObjectsWithParent(table, ObjectType.DEFAULT_CONSTRAINT); + for (DBObject df : dfs) { + statements.add("ALTER TABLE " + database.quote(name, table.name) + " DROP CONSTRAINT " + database.quote(df.name)); + } + + } + return statements; + } + + /** + * Cleans the types in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanTypes() throws SQLException { + List typeNames = + jdbcTemplate.queryForStringList( + "SELECT t.name FROM sys.types t INNER JOIN sys.schemas s ON t.schema_id = s.schema_id" + + " WHERE t.is_user_defined = 1 AND s.name = ?", + name + ); + + List statements = new ArrayList<>(); + for (String typeName : typeNames) { + statements.add("DROP TYPE " + database.quote(name, typeName)); + } + return statements; + } + + /** + * Cleans the CLR assemblies in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanAssemblies() throws SQLException { + List assemblyNames = + jdbcTemplate.queryForStringList("SELECT * FROM sys.assemblies WHERE is_user_defined=1"); + List statements = new ArrayList<>(); + for (String assemblyName : assemblyNames) { + statements.add("DROP ASSEMBLY " + database.quote(assemblyName)); + } + return statements; + } + + /** + * Cleans the triggers in this schema. + * + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanTriggers() throws SQLException { + List triggerNames = + jdbcTemplate.queryForStringList("SELECT * FROM sys.triggers" + + " WHERE is_ms_shipped=0 AND parent_id=0 AND parent_class_desc='DATABASE'"); + List statements = new ArrayList<>(); + for (String triggerName : triggerNames) { + statements.add("DROP TRIGGER " + database.quote(triggerName) + " ON DATABASE"); + } + return statements; + } + + /** + * Cleans the objects of these types in this schema. + * + * @param dropQualifier The type of DROP statement to issue. + * @param objectTypes The type of objects to drop. + * @return The drop statements. + * @throws SQLException when the clean statements could not be generated. + */ + private List cleanObjects(String dropQualifier, ObjectType... objectTypes) throws SQLException { + List statements = new ArrayList<>(); + List dbObjects = queryDBObjects(objectTypes); + for (DBObject dbObject : dbObjects) { + statements.add("DROP " + dropQualifier + " " + database.quote(name, dbObject.name)); + } + + return statements; + } + + @Override + protected SQLServerTable[] doAllTables() throws SQLException { + List tableNames = new ArrayList<>(); + for (DBObject table : queryDBObjects(ObjectType.USER_TABLE)) { + tableNames.add(table.name); + } + + SQLServerTable[] tables = new SQLServerTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new SQLServerTable(jdbcTemplate, database, databaseName, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new SQLServerTable(jdbcTemplate, database, databaseName, this, tableName); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerTable.java new file mode 100644 index 00000000..20c18786 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerTable.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sqlserver; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * SQLServer-specific table. + */ +public class SQLServerTable extends Table { + private final String databaseName; + + /** + * Creates a new SQLServer table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param databaseName The database this table lives in. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + SQLServerTable(JdbcTemplate jdbcTemplate, SQLServerDatabase database, String databaseName, SQLServerSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + this.databaseName = databaseName; + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + this); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForBoolean( + "SELECT CAST(" + + "CASE WHEN EXISTS(" + + " SELECT 1 FROM [" + databaseName + "].INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?" + + ") " + + "THEN 1 ELSE 0 " + + "END " + + "AS BIT)", + schema.getName(), name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("select * from " + this + " WITH (TABLOCKX)"); + } + + /** + * Drops system versioning for this table if it is active. + */ + void dropSystemVersioningIfPresent() throws SQLException { + /* Column temporal_type only exists in SQL Server 2016+, so the query below won't run in other versions */ + if (database.supportsTemporalTables()) { + if (jdbcTemplate.queryForInt("SELECT temporal_type FROM sys.tables WHERE object_id = OBJECT_ID('" + this + "', 'U')") == 2) { + jdbcTemplate.execute("ALTER TABLE " + this + " SET (SYSTEM_VERSIONING = OFF)"); + } + } + } + + @Override + public String toString() { + return database.quote(databaseName, schema.getName(), name); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/package-info.java new file mode 100644 index 00000000..53de07f8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.sqlserver; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEConnection.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEConnection.java new file mode 100644 index 00000000..71968b27 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEConnection.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sybasease; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +/** + * Sybase ASE Connection. + */ +public class SybaseASEConnection extends Connection { + SybaseASEConnection(SybaseASEDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + + @Override + public Schema getSchema(String name) { + //Sybase does not support schemas, nor changing users on the fly. Always return the same dummy schema. + return new SybaseASESchema(jdbcTemplate, database, "dbo"); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() { + return "dbo"; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEDatabase.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEDatabase.java new file mode 100644 index 00000000..ae375c4e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEDatabase.java @@ -0,0 +1,221 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sybasease; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.Results; +import org.flywaydb.core.internal.sqlscript.Delimiter; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Sybase ASE database. + */ +public class SybaseASEDatabase extends Database { + private static final Log LOG = LogFactory.getLog(SybaseASEDatabase.class); + + private String databaseName = null; + private boolean supportsMultiStatementTransactions = false; + + /** + * Creates a new Sybase ASE database. + * + * @param configuration The Flyway configuration. + */ + public SybaseASEDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory + + + + ) { + super(configuration, jdbcConnectionFactory + + + + ); + } + + @Override + protected SybaseASEConnection doGetConnection(Connection connection) { + return new SybaseASEConnection(this, connection); + } + + + + + + + + + + @Override + public void ensureSupported() { + ensureDatabaseIsRecentEnough("15.7"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("16.0", org.flywaydb.core.internal.license.Edition.ENTERPRISE); + + recommendFlywayUpgradeIfNecessary("16.2"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table.getName() + " (\n" + + " installed_rank INT NOT NULL,\n" + + " version VARCHAR(50) NULL,\n" + + " description VARCHAR(200) NOT NULL,\n" + + " type VARCHAR(20) NOT NULL,\n" + + " script VARCHAR(1000) NOT NULL,\n" + + " checksum INT NULL,\n" + + " installed_by VARCHAR(100) NOT NULL,\n" + + " installed_on datetime DEFAULT getDate() NOT NULL,\n" + + " execution_time INT NOT NULL,\n" + + " success decimal NOT NULL,\n" + + " PRIMARY KEY (installed_rank)\n" + + ")\n" + + "lock datarows on 'default'\n" + + (baseline ? getBaselineStatement(table) + "\n" : "") + + "go\n" + + "CREATE INDEX " + table.getName() + "_s_idx ON " + table.getName() + " (success)\n" + + "go\n"; + } + + @Override + public boolean supportsEmptyMigrationDescription() { + // Sybase will convert the empty string to a single space implicitly, which won't error on updating the + // history table but will subsequently fail validation of the history table against the file name. + return false; + } + + @Override + public Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT user_name()"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public boolean supportsChangingCurrentSchema() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + protected String doQuote(String identifier) { + //Sybase doesn't quote identifiers, skip quoting. + return identifier; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + /** + * Multi statement transaction support is dependent on the 'ddl in tran' option being set. + * However, setting 'ddl in tran' doesn't necessarily mean that multi-statement transactions are supported. + * i.e. + * - multi statement transaction support => ddl in tran + * - ddl in tran =/> multi statement transaction support + * Also, ddl in tran can change during execution for unknown reasons. + * Therefore, as a best guess: + * - When this method is called, check ddl in tran + * - If ddl in tran is true, assume support for multi statement transactions forever more + * - Never check ddl in tran again + * - If ddl in tran is false, return false + * - Check ddl in tran again on the next call + */ + public boolean supportsMultiStatementTransactions() { + if (supportsMultiStatementTransactions) { + LOG.debug("ddl in tran was found to be true at some point during execution." + + "Therefore multi statement transaction support is assumed."); + return true; + } + + boolean ddlInTran = getDdlInTranOption(); + + if (ddlInTran) { + LOG.debug("ddl in tran is true. Multi statement transaction support is now assumed."); + supportsMultiStatementTransactions = true; + } + + return supportsMultiStatementTransactions; + } + + boolean getDdlInTranOption() { + try { + // http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc36273.1600/doc/html/san1393052037182.html + String databaseName = getDatabaseName(); + // The Sybase driver (v7.07) concatenates "null" to this query and we can't see why. By adding a one-line + // comment marker we can at least prevent this causing us problems until we get a resolution. + String getDatabaseMetadataQuery = "sp_helpdb " + databaseName + " -- "; + Results results = getMainConnection().getJdbcTemplate().executeStatement(getDatabaseMetadataQuery); + for (int resultsIndex = 0; resultsIndex < results.getResults().size(); resultsIndex++) { + List columns = results.getResults().get(resultsIndex).getColumns(); + if (columns != null) { + int statusIndex = getStatusIndex(columns); + if (statusIndex > -1) { + String options = results.getResults().get(resultsIndex).getData().get(0).get(statusIndex); + return (options.contains("ddl in tran")); + } + } + } + return false; + } catch (Exception e) { + throw new FlywayException(e); + } + } + + private int getStatusIndex(List columns) { + for (int statusIndex = 0; statusIndex < columns.size(); statusIndex++) { + if ("status".equals(columns.get(statusIndex))) { + return statusIndex; + } + } + return -1; + } + + String getDatabaseName() throws SQLException { + if (databaseName == null) { + databaseName = getMainConnection().getJdbcTemplate().queryForString("select db_name()"); + } + return databaseName; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEParser.java new file mode 100644 index 00000000..3cf0a1c2 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEParser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sybasease; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.ParserContext; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.parser.PeekingReader; +import org.flywaydb.core.internal.sqlscript.Delimiter; + +import java.io.IOException; + +public class SybaseASEParser extends Parser { + public SybaseASEParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + protected boolean isDelimiter(String peek, ParserContext context, int col) { + return peek.length() >= 2 + && (peek.charAt(0) == 'G' || peek.charAt(0) == 'g') + && (peek.charAt(1) == 'O' || peek.charAt(1) == 'o') + && (peek.length() == 2 || Character.isWhitespace(peek.charAt(2))); + } + + @Override + protected String readKeyword(PeekingReader reader, Delimiter delimiter, ParserContext context) throws IOException { + // #2414: Ignore delimiter as GO (unlike ;) can be part of a regular keyword + return "" + (char) reader.read() + reader.readKeywordPart(null, context); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASESchema.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASESchema.java new file mode 100644 index 00000000..90e0e717 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASESchema.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sybasease; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.database.base.Table; + +import java.sql.SQLException; +import java.util.List; + +/** + * Sybase ASE schema (database). + */ +public class SybaseASESchema extends Schema { + SybaseASESchema(JdbcTemplate jdbcTemplate, SybaseASEDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + protected boolean doExists() throws SQLException { + //There is no schema in SAP ASE. Always return true + return true; + } + + @Override + protected boolean doEmpty() throws SQLException { + //There is no schema in SAP ASE, check whether database is empty + //Check for tables, views stored procs and triggers + return jdbcTemplate.queryForInt("select count(*) from sysobjects ob where (ob.type='U' or ob.type = 'V' or ob.type = 'P' or ob.type = 'TR') and ob.name != 'sysquerymetrics'") == 0; + } + + @Override + protected void doCreate() { + //There is no schema in SAP ASE. Do nothing for creation. + } + + @Override + protected void doDrop() throws SQLException { + //There is no schema in Sybase, no schema can be dropped. Clean instead. + doClean(); + } + + /** + * This clean method is equivalent to cleaning the whole database. + * + * @see Schema#doClean() + */ + @Override + protected void doClean() throws SQLException { + //Drop view + dropObjects("V"); + //Drop tables + dropObjects("U"); + //Drop stored procs + dropObjects("P"); + //Drop triggers + dropObjects("TR"); + } + + @Override + protected SybaseASETable[] doAllTables() throws SQLException { + //Retrieving all table names + List tableNames = retrieveAllTableNames(); + + SybaseASETable[] result = new SybaseASETable[tableNames.size()]; + + for (int i = 0; i < tableNames.size(); i++) { + String tableName = tableNames.get(i); + result[i] = new SybaseASETable(jdbcTemplate, database, this, tableName); + } + + return result; + } + + @Override + public Table getTable(String tableName) { + return new SybaseASETable(jdbcTemplate, database, this, tableName); + } + + /** + * @return all table names in the current database. + */ + private List retrieveAllTableNames() throws SQLException { + return jdbcTemplate.queryForStringList("select ob.name from sysobjects ob where ob.type=? order by ob.name", "U"); + } + + private void dropObjects(String sybaseObjType) throws SQLException { + //Getting the table names + List objNames = jdbcTemplate.queryForStringList("select ob.name from sysobjects ob where ob.type=? order by ob.name", sybaseObjType); + + //for each table, drop it + for (String name : objNames) { + String sql; + + if ("U".equals(sybaseObjType)) { + sql = "drop table "; + } else if ("V".equals(sybaseObjType)) { + sql = "drop view "; + } else if ("P".equals(sybaseObjType)) { + //dropping stored procedure + sql = "drop procedure "; + } else if ("TR".equals(sybaseObjType)) { + sql = "drop trigger "; + } else { + throw new IllegalArgumentException("Unknown database object type " + sybaseObjType); + } + + jdbcTemplate.execute(sql + name); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASETable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASETable.java new file mode 100644 index 00000000..1751818d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASETable.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.database.sybasease; + +import org.flywaydb.core.internal.database.base.SchemaObject; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +import java.sql.SQLException; + +/** + * Sybase ASE table. + */ +public class SybaseASETable extends Table { + /** + * Creates a new SAP ASE table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + SybaseASETable(JdbcTemplate jdbcTemplate, SybaseASEDatabase database, SybaseASESchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected boolean doExists() throws SQLException { + return jdbcTemplate.queryForString("SELECT object_id('" + name + "')") != null; + } + + @Override + protected void doLock() throws SQLException { + // Flyway's locking assumes transactions are being used to release locks on commit at some later point + // (hence the lack of an 'unlock' method) + // If multi statement transactions aren't supported, then locking a table makes no sense, + // since that's the only operation we can do + if (database.supportsMultiStatementTransactions()) { + jdbcTemplate.execute("LOCK TABLE " + this + " IN EXCLUSIVE MODE"); + } + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + getName()); + } + + /** + * Since Sybase ASE does not support schema, dropping out the schema name for toString method + * + * @see SchemaObject#toString() + */ + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/package-info.java new file mode 100644 index 00000000..de850643 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.database.sybasease; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywayDbUpgradeRequiredException.java b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywayDbUpgradeRequiredException.java new file mode 100644 index 00000000..44549096 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywayDbUpgradeRequiredException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.exception; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.jdbc.DatabaseType; + +/** + * Thrown when an attempt was made to migrate an outdated database version not supported by Flyway. + */ +public class FlywayDbUpgradeRequiredException extends FlywayException { + public FlywayDbUpgradeRequiredException(DatabaseType databaseType, String version, String minimumVersion) { + super(databaseType + " upgrade required: " + databaseType + " " + version + + " is outdated and no longer supported by Flyway. Flyway currently supports " + databaseType + " " + + minimumVersion + " and newer."); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywaySqlException.java b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywaySqlException.java new file mode 100644 index 00000000..315a0c82 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywaySqlException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.exception; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.util.ExceptionUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.sql.SQLException; + +/** + * This specific exception thrown when Flyway encounters a problem in SQL statement. + */ +public class FlywaySqlException extends FlywayException { + /** + * Creates new instance of FlywaySqlScriptException. + * + * @param sqlException Cause of the problem. + */ + public FlywaySqlException(String message, SQLException sqlException) { + super(message, sqlException, ErrorCode.DB_CONNECTION); + } + + @Override + public String getMessage() { + String title = super.getMessage(); + String underline = StringUtils.trimOrPad("", title.length(), '-'); + + return "\n" + title + "\n" + underline + "\n" + ExceptionUtils.toMessage((SQLException) getCause()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/package-info.java new file mode 100644 index 00000000..87e27376 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.exception; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/AppliedMigrationAttributes.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/AppliedMigrationAttributes.java new file mode 100644 index 00000000..841cda3f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/AppliedMigrationAttributes.java @@ -0,0 +1,23 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.info; + +class AppliedMigrationAttributes { + public boolean outOfOrder; + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoContext.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoContext.java new file mode 100644 index 00000000..6bf39b89 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoContext.java @@ -0,0 +1,115 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.info; + +import org.flywaydb.core.api.MigrationVersion; + +import java.util.HashMap; +import java.util.Map; + +/** + * The current context of the migrations. + */ +public class MigrationInfoContext { + /** + * Whether out of order migrations are allowed. + */ + public boolean outOfOrder; + + /** + * Whether pending migrations are allowed. + */ + public boolean pending; + + /** + * Whether missing migrations are allowed. + */ + public boolean missing; + + /** + * Whether ignored migrations are allowed. + */ + public boolean ignored; + + /** + * Whether future migrations are allowed. + */ + public boolean future; + + /** + * The migration target. + */ + public MigrationVersion target; + + /** + * The SCHEMA migration version that was applied. + */ + public MigrationVersion schema; + + /** + * The BASELINE migration version that was applied. + */ + public MigrationVersion baseline; + + /** + * The last resolved migration. + */ + public MigrationVersion lastResolved = MigrationVersion.EMPTY; + + /** + * The last applied migration. + */ + public MigrationVersion lastApplied = MigrationVersion.EMPTY; + + public Map latestRepeatableRuns = new HashMap<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MigrationInfoContext that = (MigrationInfoContext) o; + + if (outOfOrder != that.outOfOrder) return false; + if (pending != that.pending) return false; + if (missing != that.missing) return false; + if (ignored != that.ignored) return false; + if (future != that.future) return false; + if (target != null ? !target.equals(that.target) : that.target != null) return false; + if (schema != null ? !schema.equals(that.schema) : that.schema != null) return false; + if (baseline != null ? !baseline.equals(that.baseline) : that.baseline != null) return false; + if (lastResolved != null ? !lastResolved.equals(that.lastResolved) : that.lastResolved != null) return false; + if (lastApplied != null ? !lastApplied.equals(that.lastApplied) : that.lastApplied != null) return false; + return latestRepeatableRuns.equals(that.latestRepeatableRuns); + + } + + @Override + public int hashCode() { + int result = (outOfOrder ? 1 : 0); + result = 31 * result + (pending ? 1 : 0); + result = 31 * result + (missing ? 1 : 0); + result = 31 * result + (ignored ? 1 : 0); + result = 31 * result + (future ? 1 : 0); + result = 31 * result + (target != null ? target.hashCode() : 0); + result = 31 * result + (schema != null ? schema.hashCode() : 0); + result = 31 * result + (baseline != null ? baseline.hashCode() : 0); + result = 31 * result + (lastResolved != null ? lastResolved.hashCode() : 0); + result = 31 * result + (lastApplied != null ? lastApplied.hashCode() : 0); + result = 31 * result + latestRepeatableRuns.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoDumper.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoDumper.java new file mode 100644 index 00000000..e4621965 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoDumper.java @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.info; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationState; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.internal.util.AsciiTable; +import org.flywaydb.core.internal.util.DateUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Dumps migrations in an ascii-art table in the logs and the console. + */ +public class MigrationInfoDumper { + /** + * Prevent instantiation. + */ + private MigrationInfoDumper() { + // Do nothing + } + + /** + * Dumps the info about all migrations into an ascii table. + * + * @param migrationInfos The list of migrationInfos to dump. + * @return The ascii table, as one big multi-line string. + */ + public static String dumpToAsciiTable(MigrationInfo[] migrationInfos) { + + + + + + List columns = Arrays.asList("Category", "Version", "Description", "Type", "Installed On", "State" + + + + ); + + List> rows = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + List row = Arrays.asList( + getCategory(migrationInfo), + getVersionStr(migrationInfo), + migrationInfo.getDescription(), + migrationInfo.getType().name(), + DateUtils.formatDateAsIsoString(migrationInfo.getInstalledOn()), + migrationInfo.getState().getDisplayName() + + + + ); + rows.add(row); + } + + return new AsciiTable(columns, rows, true, "", "No migrations found").render(); + } + + static String getCategory(MigrationInfo migrationInfo) { + if (migrationInfo.getType().isSynthetic()) { + return ""; + } + if (migrationInfo.getVersion() == null) { + return "Repeatable"; + } + + + + + + return "Versioned"; + } + + private static String getVersionStr(MigrationInfo migrationInfo) { + return migrationInfo.getVersion() == null ? "" : migrationInfo.getVersion().toString(); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoImpl.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoImpl.java new file mode 100644 index 00000000..63fdc990 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoImpl.java @@ -0,0 +1,457 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.info; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationState; +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl; +import org.flywaydb.core.internal.schemahistory.AppliedMigration; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.AbbreviationUtils; + +import java.util.Date; + +/** + * Default implementation of MigrationInfo. + */ +public class MigrationInfoImpl implements MigrationInfo { + /** + * The resolved migration to aggregate the info from. + */ + private final ResolvedMigration resolvedMigration; + + /** + * The applied migration to aggregate the info from. + */ + private final AppliedMigration appliedMigration; + + /** + * The current context. + */ + private final MigrationInfoContext context; + + /** + * Whether this migration was applied out of order. + */ + private final boolean outOfOrder; + + + + + + + + + /** + * Creates a new MigrationInfoImpl. + * + * @param resolvedMigration The resolved migration to aggregate the info from. + * @param appliedMigration The applied migration to aggregate the info from. + * @param context The current context. + * @param outOfOrder Whether this migration was applied out of order. + + + + */ + MigrationInfoImpl(ResolvedMigration resolvedMigration, AppliedMigration appliedMigration, + MigrationInfoContext context, boolean outOfOrder + + + + ) { + this.resolvedMigration = resolvedMigration; + this.appliedMigration = appliedMigration; + this.context = context; + this.outOfOrder = outOfOrder; + + + + } + + /** + * @return The resolved migration to aggregate the info from. + */ + public ResolvedMigration getResolvedMigration() { + return resolvedMigration; + } + + /** + * @return The applied migration to aggregate the info from. + */ + public AppliedMigration getAppliedMigration() { + return appliedMigration; + } + + @Override + public MigrationType getType() { + if (appliedMigration != null) { + return appliedMigration.getType(); + } + return resolvedMigration.getType(); + } + + @Override + public Integer getChecksum() { + if (appliedMigration != null) { + return appliedMigration.getChecksum(); + } + return resolvedMigration.getChecksum(); + } + + @Override + public MigrationVersion getVersion() { + if (appliedMigration != null) { + return appliedMigration.getVersion(); + } + return resolvedMigration.getVersion(); + } + + @Override + public String getDescription() { + if (appliedMigration != null) { + return appliedMigration.getDescription(); + } + return resolvedMigration.getDescription(); + } + + @Override + public String getScript() { + if (appliedMigration != null) { + return appliedMigration.getScript(); + } + return resolvedMigration.getScript(); + } + + @Override + public MigrationState getState() { + + + + + + + if (appliedMigration == null) { + if (resolvedMigration.getVersion() != null) { + if (resolvedMigration.getVersion().compareTo(context.baseline) < 0) { + return MigrationState.BELOW_BASELINE; + } + + + + + + if (context.target != null && resolvedMigration.getVersion().compareTo(context.target) > 0) { + return MigrationState.ABOVE_TARGET; + } + if ((resolvedMigration.getVersion().compareTo(context.lastApplied) < 0) && !context.outOfOrder) { + return MigrationState.IGNORED; + } + } + return MigrationState.PENDING; + } + + if (MigrationType.BASELINE == appliedMigration.getType()) { + return MigrationState.BASELINE; + } + + if (resolvedMigration == null) { + if (MigrationType.SCHEMA == appliedMigration.getType()) { + return MigrationState.SUCCESS; + } + + if ((appliedMigration.getVersion() == null) || getVersion().compareTo(context.lastResolved) < 0) { + if (appliedMigration.isSuccess()) { + return MigrationState.MISSING_SUCCESS; + } + return MigrationState.MISSING_FAILED; + } else { + if (appliedMigration.isSuccess()) { + return MigrationState.FUTURE_SUCCESS; + } + return MigrationState.FUTURE_FAILED; + } + } + + if (!appliedMigration.isSuccess()) { + return MigrationState.FAILED; + } + + if (appliedMigration.getVersion() == null) { + if (appliedMigration.getInstalledRank() == context.latestRepeatableRuns.get(appliedMigration.getDescription())) { + if (resolvedMigration.checksumMatches(appliedMigration.getChecksum())) { + return MigrationState.SUCCESS; + } + return MigrationState.OUTDATED; + } + return MigrationState.SUPERSEDED; + } + + if (outOfOrder) { + return MigrationState.OUT_OF_ORDER; + } + return MigrationState.SUCCESS; + } + + @Override + public Date getInstalledOn() { + if (appliedMigration != null) { + return appliedMigration.getInstalledOn(); + } + return null; + } + + @Override + public String getInstalledBy() { + if (appliedMigration != null) { + return appliedMigration.getInstalledBy(); + } + return null; + } + + @Override + public Integer getInstalledRank() { + if (appliedMigration != null) { + return appliedMigration.getInstalledRank(); + } + return null; + } + + @Override + public Integer getExecutionTime() { + if (appliedMigration != null) { + return appliedMigration.getExecutionTime(); + } + return null; + } + + @Override + public String getPhysicalLocation() { + if (resolvedMigration != null) { + return resolvedMigration.getPhysicalLocation(); + } + return ""; + } + + /** + * Validates this migrationInfo for consistency. + * + * @return The error message, or {@code null} if everything is fine. + */ + public String validate() { + MigrationState state = getState(); + + + + + + + + + // Ignore any migrations above the current target as they are out of scope. + if (MigrationState.ABOVE_TARGET.equals(state)) { + return null; + } + + if (state.isFailed() && (!context.future || MigrationState.FUTURE_FAILED != state)) { + if (getVersion() == null) { + return "Detected failed repeatable migration: " + getDescription(); + } + return "Detected failed migration to version " + getVersion() + " (" + getDescription() + ")"; + } + + if ((resolvedMigration == null) + && !appliedMigration.getType().isSynthetic() + + + + && (appliedMigration.getVersion() != null) + && (!context.missing || (MigrationState.MISSING_SUCCESS != state && MigrationState.MISSING_FAILED != state)) + && (!context.future || (MigrationState.FUTURE_SUCCESS != state && MigrationState.FUTURE_FAILED != state))) { + return "Detected applied migration not resolved locally: " + getVersion(); + } + + if (!context.pending && MigrationState.PENDING == state || (!context.ignored && MigrationState.IGNORED == state)) { + if (getVersion() != null) { + return "Detected resolved migration not applied to database: " + getVersion(); + } + return "Detected resolved repeatable migration not applied to database: " + getDescription(); + } + + if (!context.pending && MigrationState.OUTDATED == state) { + return "Detected outdated resolved repeatable migration that should be re-applied to database: " + getDescription(); + } + + if (resolvedMigration != null && appliedMigration != null + + + + ) { + String migrationIdentifier = appliedMigration.getVersion() == null ? + // Repeatable migrations + appliedMigration.getScript() : + // Versioned migrations + "version " + appliedMigration.getVersion(); + if (getVersion() == null || getVersion().compareTo(context.baseline) > 0) { + if (resolvedMigration.getType() != appliedMigration.getType()) { + return createMismatchMessage("type", migrationIdentifier, + appliedMigration.getType(), resolvedMigration.getType()); + } + if (resolvedMigration.getVersion() != null + || (context.pending && MigrationState.OUTDATED != state && MigrationState.SUPERSEDED != state)) { + if (!resolvedMigration.checksumMatches(appliedMigration.getChecksum())) { + return createMismatchMessage("checksum", migrationIdentifier, + appliedMigration.getChecksum(), resolvedMigration.getChecksum()); + } + } + if (descriptionMismatch(resolvedMigration, appliedMigration)) { + return createMismatchMessage("description", migrationIdentifier, + appliedMigration.getDescription(), resolvedMigration.getDescription()); + } + } + } + + // Perform additional validation for pending migrations. This is not performed for previously applied migrations + // as it is assumed that if the checksum is unchanged, previous positive validation results still hold true. + // #2392: Migrations above target are also ignored as the user explicitly asked for them to not be taken into account. + if (!context.pending && MigrationState.PENDING == state && resolvedMigration instanceof ResolvedMigrationImpl) { + ((ResolvedMigrationImpl) resolvedMigration).validate(); + } + + return null; + } + + private boolean descriptionMismatch(ResolvedMigration resolvedMigration, AppliedMigration appliedMigration) { + // For some databases, we can't put an empty description into the history table + if (SchemaHistory.NO_DESCRIPTION_MARKER.equals(appliedMigration.getDescription())) { + return !"".equals(resolvedMigration.getDescription()); + } + // The default + return (!AbbreviationUtils.abbreviateDescription(resolvedMigration.getDescription()) + .equals(appliedMigration.getDescription())); + } + + /** + * Creates a message for a mismatch. + * + * @param mismatch The type of mismatch. + * @param migrationIdentifier The offending version. + * @param applied The applied value. + * @param resolved The resolved value. + * @return The message. + */ + private String createMismatchMessage(String mismatch, String migrationIdentifier, Object applied, Object resolved) { + return String.format("Migration " + mismatch + " mismatch for migration %s\n" + + "-> Applied to database : %s\n" + + "-> Resolved locally : %s", + migrationIdentifier, applied, resolved); + } + + @Override + public int compareTo(MigrationInfo o) { + if ((getInstalledRank() != null) && (o.getInstalledRank() != null)) { + return getInstalledRank() - o.getInstalledRank(); + } + + MigrationState state = getState(); + MigrationState oState = o.getState(); + + // Below baseline migrations come before applied ones + if (state == MigrationState.BELOW_BASELINE && oState.isApplied()) { + return -1; + } + if (state.isApplied() && oState == MigrationState.BELOW_BASELINE) { + return 1; + } + + if (state == MigrationState.IGNORED && oState.isApplied()) { + if (getVersion() != null && o.getVersion() != null) { + return getVersion().compareTo(o.getVersion()); + } + return -1; + } + if (state.isApplied() && oState == MigrationState.IGNORED) { + if (getVersion() != null && o.getVersion() != null) { + return getVersion().compareTo(o.getVersion()); + } + return 1; + } + + // Sort installed before pending + if (getInstalledRank() != null) { + return -1; + } + if (o.getInstalledRank() != null) { + return 1; + } + + // No migration installed, sort according to other criteria + // Two versioned migrations: sort by version + if (getVersion() != null && o.getVersion() != null) { + int v = getVersion().compareTo(o.getVersion()); + if (v != 0) { + return v; + } + + + + + + + + + + + + return 0; + } + + // One versioned and one repeatable migration: versioned migration goes before repeatable one + if (getVersion() != null) { + return -1; + } + if (o.getVersion() != null) { + return 1; + } + + // Two repeatable migrations: sort by description + return getDescription().compareTo(o.getDescription()); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MigrationInfoImpl that = (MigrationInfoImpl) o; + + if (appliedMigration != null ? !appliedMigration.equals(that.appliedMigration) : that.appliedMigration != null) + return false; + if (!context.equals(that.context)) return false; + return !(resolvedMigration != null ? !resolvedMigration.equals(that.resolvedMigration) : that.resolvedMigration != null); + } + + @Override + public int hashCode() { + int result = resolvedMigration != null ? resolvedMigration.hashCode() : 0; + result = 31 * result + (appliedMigration != null ? appliedMigration.hashCode() : 0); + result = 31 * result + context.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoServiceImpl.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoServiceImpl.java new file mode 100644 index 00000000..130e7ee2 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoServiceImpl.java @@ -0,0 +1,468 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.info; + +import org.flywaydb.core.api.*; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.output.InfoOutput; +import org.flywaydb.core.internal.output.InfoOutputFactory; +import org.flywaydb.core.internal.schemahistory.AppliedMigration; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.flywaydb.core.internal.util.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; + +/** + * Default implementation of MigrationInfoService. + */ +public class MigrationInfoServiceImpl implements MigrationInfoService { + /** + * The migration resolver for available migrations. + */ + private final MigrationResolver migrationResolver; + + private final Context context; + + /** + * The schema history table for applied migrations. + */ + private final SchemaHistory schemaHistory; + + /** + * The target version up to which to retrieve the info. + */ + private MigrationVersion target; + + /** + * Allows migrations to be run "out of order". + *

If you already have versions 1 and 3 applied, and now a version 2 is found, + * it will be applied too instead of being ignored.

+ *

(default: {@code false})

+ */ + private boolean outOfOrder; + + /** + * Whether pending migrations are allowed. + */ + private final boolean pending; + + /** + * Whether missing migrations are allowed. + */ + private final boolean missing; + + /** + * Whether ignored migrations are allowed. + */ + private final boolean ignored; + + /** + * Whether future migrations are allowed. + */ + private final boolean future; + + /** + * The migrations infos calculated at the last refresh. + */ + private List migrationInfos; + + /** + * Creates a new MigrationInfoServiceImpl. + * + * @param migrationResolver The migration resolver for available migrations. + * @param schemaHistory The schema history table for applied migrations. + * @param configuration The current configuration. + * @param target The target version up to which to retrieve the info. + * @param outOfOrder Allows migrations to be run "out of order". + * @param pending Whether pending migrations are allowed. + * @param missing Whether missing migrations are allowed. + * @param ignored Whether ignored migrations are allowed. + * @param future Whether future migrations are allowed. + */ + public MigrationInfoServiceImpl(MigrationResolver migrationResolver, + SchemaHistory schemaHistory, final Configuration configuration, + MigrationVersion target, boolean outOfOrder, + boolean pending, boolean missing, boolean ignored, boolean future) { + this.migrationResolver = migrationResolver; + this.schemaHistory = schemaHistory; + this.context = new Context() { + @Override + public Configuration getConfiguration() { + return configuration; + } + }; + this.target = target; + this.outOfOrder = outOfOrder; + this.pending = pending; + this.missing = missing; + this.ignored = ignored; + this.future = future; + } + + /** + * Refreshes the info about all known migrations from both the classpath and the DB. + */ + public void refresh() { + Collection resolvedMigrations = migrationResolver.resolveMigrations(context); + List appliedMigrations = schemaHistory.allAppliedMigrations(); + + MigrationInfoContext context = new MigrationInfoContext(); + context.outOfOrder = outOfOrder; + context.pending = pending; + context.missing = missing; + context.ignored = ignored; + context.future = future; + context.target = target; + + Map, ResolvedMigration> resolvedVersioned = new TreeMap<>(); + Map resolvedRepeatable = new TreeMap<>(); + + for (ResolvedMigration resolvedMigration : resolvedMigrations) { + MigrationVersion version = resolvedMigration.getVersion(); + if (version != null) { + if (version.compareTo(context.lastResolved) > 0) { + context.lastResolved = version; + } + //noinspection RedundantConditionalExpression + resolvedVersioned.put(Pair.of(version, + + + + false), resolvedMigration); + } else { + resolvedRepeatable.put(resolvedMigration.getDescription(), resolvedMigration); + } + } + + List> appliedVersioned = new ArrayList<>(); + List appliedRepeatable = new ArrayList<>(); + for (AppliedMigration appliedMigration : appliedMigrations) { + MigrationVersion version = appliedMigration.getVersion(); + if (version == null) { + appliedRepeatable.add(appliedMigration); + continue; + } + if (appliedMigration.getType() == MigrationType.SCHEMA) { + context.schema = version; + } + if (appliedMigration.getType() == MigrationType.BASELINE) { + context.baseline = version; + } + + + + + + appliedVersioned.add(Pair.of(appliedMigration, new AppliedMigrationAttributes())); + } + + for (Pair av : appliedVersioned) { + MigrationVersion version = av.getLeft().getVersion(); + if (version != null) { + if (version.compareTo(context.lastApplied) > 0) { + + + + context.lastApplied = version; + + + + } else { + av.getRight().outOfOrder = true; + } + } + } + + if (MigrationVersion.CURRENT == target) { + context.target = context.lastApplied; + } + + List migrationInfos1 = new ArrayList<>(); + Set pendingResolvedVersioned = new HashSet<>(resolvedVersioned.values()); + for (Pair av : appliedVersioned) { + ResolvedMigration resolvedMigration = resolvedVersioned.get(Pair.of(av.getLeft().getVersion(), av.getLeft().getType().isUndo())); + if (resolvedMigration != null + + + + ) { + pendingResolvedVersioned.remove(resolvedMigration); + } + migrationInfos1.add(new MigrationInfoImpl(resolvedMigration, av.getLeft(), context, av.getRight().outOfOrder + + + + )); + } + + for (ResolvedMigration prv : pendingResolvedVersioned) { + migrationInfos1.add(new MigrationInfoImpl(prv, null, context, false + + + + )); + } + + + for (AppliedMigration appliedRepeatableMigration : appliedRepeatable) { + if (!context.latestRepeatableRuns.containsKey(appliedRepeatableMigration.getDescription()) + || (appliedRepeatableMigration.getInstalledRank() > context.latestRepeatableRuns.get(appliedRepeatableMigration.getDescription()))) { + context.latestRepeatableRuns.put(appliedRepeatableMigration.getDescription(), appliedRepeatableMigration.getInstalledRank()); + } + } + + Set pendingResolvedRepeatable = new HashSet<>(resolvedRepeatable.values()); + for (AppliedMigration appliedRepeatableMigration : appliedRepeatable) { + ResolvedMigration resolvedMigration = resolvedRepeatable.get(appliedRepeatableMigration.getDescription()); + int latestRank = context.latestRepeatableRuns.get(appliedRepeatableMigration.getDescription()); + if (resolvedMigration != null && appliedRepeatableMigration.getInstalledRank() == latestRank + && resolvedMigration.checksumMatches(appliedRepeatableMigration.getChecksum())) { + pendingResolvedRepeatable.remove(resolvedMigration); + } + migrationInfos1.add(new MigrationInfoImpl(resolvedMigration, appliedRepeatableMigration, context, false + + + + )); + } + + for (ResolvedMigration prr : pendingResolvedRepeatable) { + migrationInfos1.add(new MigrationInfoImpl(prr, null, context, false + + + + )); + } + + Collections.sort(migrationInfos1); + migrationInfos = migrationInfos1; + } + + + + + + + + + + + + + + + + + + + + + + + + + + @Override + public MigrationInfo[] all() { + return migrationInfos.toArray(new MigrationInfo[0]); + } + + @Override + public MigrationInfo current() { + MigrationInfo current = null; + for (MigrationInfoImpl migrationInfo : migrationInfos) { + if (migrationInfo.getState().isApplied() + + + + + && migrationInfo.getVersion() != null + && (current == null || migrationInfo.getVersion().compareTo(current.getVersion()) > 0)) { + current = migrationInfo; + } + } + if (current != null) { + return current; + } + + // If no versioned migration has been applied so far, fall back to the latest repeatable one + for (int i = migrationInfos.size() - 1; i >= 0; i--) { + MigrationInfoImpl migrationInfo = migrationInfos.get(i); + if (migrationInfo.getState().isApplied() + + + + + ) { + return migrationInfo; + } + } + + return null; + } + + @Override + public MigrationInfoImpl[] pending() { + List pendingMigrations = new ArrayList<>(); + for (MigrationInfoImpl migrationInfo : migrationInfos) { + if (MigrationState.PENDING == migrationInfo.getState()) { + pendingMigrations.add(migrationInfo); + } + } + + return pendingMigrations.toArray(new MigrationInfoImpl[0]); + } + + @Override + public MigrationInfoImpl[] applied() { + List appliedMigrations = new ArrayList<>(); + for (MigrationInfoImpl migrationInfo : migrationInfos) { + if (migrationInfo.getState().isApplied()) { + appliedMigrations.add(migrationInfo); + } + } + + return appliedMigrations.toArray(new MigrationInfoImpl[0]); + } + + /** + * Retrieves the full set of infos about the migrations resolved on the classpath. + * + * @return The resolved migrations. An empty array if none. + */ + public MigrationInfo[] resolved() { + List resolvedMigrations = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + if (migrationInfo.getState().isResolved()) { + resolvedMigrations.add(migrationInfo); + } + } + + return resolvedMigrations.toArray(new MigrationInfo[0]); + } + + /** + * Retrieves the full set of infos about the migrations that failed. + * + * @return The failed migrations. An empty array if none. + */ + public MigrationInfo[] failed() { + List failedMigrations = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + if (migrationInfo.getState().isFailed()) { + failedMigrations.add(migrationInfo); + } + } + + return failedMigrations.toArray(new MigrationInfo[0]); + } + + /** + * Retrieves the full set of infos about future migrations applied to the DB. + * + * @return The future migrations. An empty array if none. + */ + public MigrationInfo[] future() { + List futureMigrations = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + if (((migrationInfo.getState() == MigrationState.FUTURE_SUCCESS) + || (migrationInfo.getState() == MigrationState.FUTURE_FAILED)) + + + + + ) { + futureMigrations.add(migrationInfo); + } + } + + return futureMigrations.toArray(new MigrationInfo[0]); + } + + /** + * Retrieves the full set of infos about out of order migrations applied to the DB. + * + * @return The out of order migrations. An empty array if none. + */ + public MigrationInfo[] outOfOrder() { + List outOfOrderMigrations = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + if (migrationInfo.getState() == MigrationState.OUT_OF_ORDER) { + outOfOrderMigrations.add(migrationInfo); + } + } + + return outOfOrderMigrations.toArray(new MigrationInfo[0]); + } + + + + + + + + + + + + + + + + + + + + + /** + * Validate all migrations for consistency. + * + * @return The error message, or {@code null} if everything is fine. + */ + public String validate() { + StringBuilder builder = new StringBuilder(); + boolean hasFailures = false; + + for (MigrationInfoImpl migrationInfo : migrationInfos) { + String message = migrationInfo.validate(); + if (message != null) { + if (!hasFailures) + builder.append("\n"); + + builder.append(message + "\n"); + hasFailures = true; + } + } + return (hasFailures) ? builder.toString() : null; + } + + @Override + public InfoOutput getInfoOutput() { + InfoOutputFactory infoOutputFactory = new InfoOutputFactory(); + return infoOutputFactory.create(this.context.getConfiguration(), this.all(), this.current()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/info/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/info/package-info.java new file mode 100644 index 00000000..a152bf85 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/info/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.info; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/CockroachRetryingTransactionalExecutionTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/CockroachRetryingTransactionalExecutionTemplate.java new file mode 100644 index 00000000..bca90e01 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/CockroachRetryingTransactionalExecutionTemplate.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing transactions. Cockroach always operates with transaction isolation + * level SERIALIZABLE and needs a retrying pattern. + */ +public class CockroachRetryingTransactionalExecutionTemplate extends TransactionalExecutionTemplate { + private static final Log LOG = LogFactory.getLog(CockroachRetryingTransactionalExecutionTemplate.class); + + private static final String DEADLOCK_OR_TIMEOUT_ERROR_CODE = "40001"; + private static final int MAX_RETRIES = 50; + + /** + * Creates a new transaction template for this connection. + * + * @param connection The connection for the transaction. + * @param rollbackOnException Whether to roll back the transaction when an exception is thrown. + */ + CockroachRetryingTransactionalExecutionTemplate(Connection connection, boolean rollbackOnException) { + super(connection, rollbackOnException); + } + + /** + * Executes this callback within a transaction + * + * @param transactionCallback The callback to execute. + * @return The result of the transaction code. + */ + @Override + public T execute(Callable transactionCallback) { + // Similar in approach to the CockroachDBRetryingStrategy pattern + int retryCount = 0; + while (true) { + try { + return transactionCallback.call(); + } catch (SQLException e) { + if (!DEADLOCK_OR_TIMEOUT_ERROR_CODE.equals(e.getSQLState()) || retryCount >= MAX_RETRIES) { + LOG.info("error: " + e); + throw new FlywayException(e); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new FlywayException(e); + } + retryCount++; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DatabaseType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DatabaseType.java new file mode 100644 index 00000000..31122e30 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DatabaseType.java @@ -0,0 +1,207 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * The various types of databases Flyway supports. + */ +@SuppressWarnings("SqlDialectInspection") +public enum DatabaseType { + COCKROACHDB("CockroachDB", Types.NULL, false), + DB2("DB2", Types.VARCHAR, true), + + + + DERBY("Derby", Types.VARCHAR, true), + FIREBIRD("Firebird", Types.NULL, true), // TODO does it support read only transactions? + H2("H2", Types.VARCHAR, true), + HSQLDB("HSQLDB", Types.VARCHAR, true), + INFORMIX("Informix", Types.VARCHAR, true), + MARIADB("MariaDB", Types.VARCHAR, true), + MYSQL("MySQL", Types.VARCHAR, true), + ORACLE("Oracle", Types.VARCHAR, true), + POSTGRESQL("PostgreSQL", Types.NULL, true), + REDSHIFT("Redshift", Types.VARCHAR, true), + SQLITE("SQLite", Types.VARCHAR, false), + SQLSERVER("SQL Server", Types.VARCHAR, true), + SYBASEASE_JTDS("Sybase ASE", Types.NULL, true), + SYBASEASE_JCONNECT("Sybase ASE", Types.VARCHAR, true), + SAPHANA("SAP HANA", Types.VARCHAR, true), + SNOWFLAKE("Snowflake", Types.VARCHAR, false); + + private final String name; + + private final int nullType; + + private final boolean supportsReadOnlyTransactions; + + DatabaseType(String name, int nullType, boolean supportsReadOnlyTransactions) { + this.name = name; + this.nullType = nullType; + this.supportsReadOnlyTransactions = supportsReadOnlyTransactions; + } + + public static DatabaseType fromJdbcConnection(Connection connection) { + DatabaseMetaData databaseMetaData = JdbcUtils.getDatabaseMetaData(connection); + String databaseProductName = JdbcUtils.getDatabaseProductName(databaseMetaData); + String databaseProductVersion = JdbcUtils.getDatabaseProductVersion(databaseMetaData); + + return fromDatabaseProductNameAndVersion(databaseProductName, databaseProductVersion, connection); + } + + private static DatabaseType fromDatabaseProductNameAndVersion(String databaseProductName, + String databaseProductVersion, + Connection connection) { + if (databaseProductName.startsWith("Apache Derby")) { + return DERBY; + } + if (databaseProductName.startsWith("SQLite")) { + return SQLITE; + } + if (databaseProductName.startsWith("H2")) { + return H2; + } + if (databaseProductName.contains("HSQL Database Engine")) { + return HSQLDB; + } + if (databaseProductName.startsWith("Microsoft SQL Server")) { + return SQLSERVER; + } + + // #2289: MariaDB JDBC driver 2.4.0 and newer report MariaDB as "MariaDB" + if (databaseProductName.startsWith("MariaDB") + // Older versions of the driver report MariaDB as "MySQL" + || (databaseProductName.contains("MySQL") && databaseProductVersion.contains("MariaDB")) + // Azure Database For MariaDB reports as "MySQL" + || (databaseProductName.contains("MySQL") && getSelectVersionOutput(connection).contains("MariaDB"))) { + return MARIADB; + } + + if (databaseProductName.contains("MySQL")) { + // Google Cloud SQL returns different names depending on the environment and the SDK version. + // ex.: Google SQL Service/MySQL + return MYSQL; + } + if (databaseProductName.startsWith("Oracle")) { + return ORACLE; + } + if (databaseProductName.startsWith("PostgreSQL")) { + String selectVersionQueryOutput = getSelectVersionOutput(connection); + if (databaseProductName.startsWith("PostgreSQL 8") && selectVersionQueryOutput.contains("Redshift")) { + return REDSHIFT; + } + if (selectVersionQueryOutput.contains("CockroachDB")) { + return COCKROACHDB; + } + return POSTGRESQL; + } + if (databaseProductName.startsWith("DB2")) { + + + + + + return DB2; + } + if (databaseProductName.startsWith("ASE")) { + return SYBASEASE_JTDS; + } + if (databaseProductName.startsWith("Adaptive Server Enterprise")) { + return SYBASEASE_JCONNECT; + } + if (databaseProductName.startsWith("HDB")) { + return SAPHANA; + } + if (databaseProductName.startsWith("Informix")) { + return INFORMIX; + } + if (databaseProductName.startsWith("Firebird")) { + return FIREBIRD; + } + if (databaseProductName.startsWith("Snowflake")) { + return SNOWFLAKE; + } + throw new FlywayException("Unsupported Database: " + databaseProductName); + } + + /** + * Retrieves the version string for this connection as described by SELECT VERSION(), which may differ + * from the connection metadata. + * + * @param connection The connection to use. + * @return The version string. + */ + public static String getSelectVersionOutput(Connection connection) { + PreparedStatement statement = null; + ResultSet resultSet = null; + + String result; + try { + statement = connection.prepareStatement("SELECT version()"); + resultSet = statement.executeQuery(); + result = null; + if (resultSet.next()) { + result = resultSet.getString(1); + } + } catch (SQLException e) { + return ""; + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * @return The human-readable name for this database. + */ + public String getName() { + return name; + } + + /** + * @return The JDBC type used to represent {@code null} in prepared statements. + */ + public int getNullType() { + return nullType; + } + + @Override + public String toString() { + return name; + } + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DriverDataSource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DriverDataSource.java new file mode 100644 index 00000000..763a5c9a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DriverDataSource.java @@ -0,0 +1,555 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.FeatureDetector; +import org.flywaydb.core.internal.util.StringUtils; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Properties; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * YAGNI: The simplest DataSource implementation that works for Flyway. + */ +public class DriverDataSource implements DataSource { + private static final Log LOG = LogFactory.getLog(DriverDataSource.class); + + /** + * The driver types that flyway supports. Contains the jdbc prefix and the driver class name. + * + * NOTE: The drivers will be matched in order, from the top of this enum down. + */ + public enum DriverType { + DB2("jdbc:db2:", "com.ibm.db2.jcc.DB2Driver"), + DERBY_CLIENT("jdbc:derby://", "org.apache.derby.jdbc.ClientDriver"), + DERBY_EMBEDDED("jdbc:derby:", "org.apache.derby.jdbc.EmbeddedDriver"), + FIREBIRD("jdbc:firebird:", "org.firebirdsql.jdbc.FBDriver"), + FIREBIRD_SQL("jdbc:firebirdsql:", "org.firebirdsql.jdbc.FBDriver"), + H2("jdbc:h2:", "org.h2.Driver"), + HSQL("jdbc:hsqldb:", "org.hsqldb.jdbcDriver"), + INFORMIX("jdbc:informix-sqli:", "com.informix.jdbc.IfxDriver"), + JTDS("jdbc:jtds:", "net.sourceforge.jtds.jdbc.Driver"), + MARIADB("jdbc:mariadb:", "org.mariadb.jdbc.Driver"), + MYSQL("jdbc:mysql:", "com.mysql.cj.jdbc.Driver"), + MYSQL_GOOGLE("jdbc:google:", "com.mysql.jdbc.GoogleDriver"), + ORACLE("jdbc:oracle", "oracle.jdbc.OracleDriver"), + POSTGRESQL("jdbc:postgresql:", "org.postgresql.Driver"), + REDSHIFT("jdbc:redshift:", "com.amazon.redshift.jdbc42.Driver"), + SAPHANA("jdbc:sap:", "com.sap.db.jdbc.Driver"), + SNOWFLAKE("jdbc:snowflake:", "net.snowflake.client.jdbc.SnowflakeDriver"), + SQLDROID("jdbc:sqldroid:", "org.sqldroid.SQLDroidDriver"), + SQLLITE("jdbc:sqlite:", "org.sqlite.JDBC"), + SQLSERVER("jdbc:sqlserver:", "com.microsoft.sqlserver.jdbc.SQLServerDriver"), + SYBASE("jdbc:sybase:", "com.sybase.jdbc4.jdbc.SybDriver"), + TEST_CONTAINERS("jdbc:tc:", "org.testcontainers.jdbc.ContainerDatabaseDriver"); + + DriverType(String prefix, String driverClass) { + this.prefix = prefix; + this.driverClass = driverClass; + } + + public String prefix; + public String driverClass; + + public boolean matches(String url) { + return url.startsWith(prefix); + } + } + + private static final String MYSQL_LEGACY_JDBC_DRIVER = "com.mysql.jdbc.Driver"; + private static final String REDSHIFT_JDBC4_DRIVER = "com.amazon.redshift.jdbc4.Driver"; + private static final String REDSHIFT_JDBC41_DRIVER = "com.amazon.redshift.jdbc41.Driver"; + + /** + * The name of the application that created the connection. This is useful for databases that allow setting this + * in order to easily correlate individual application with database connections. + */ + private static final String APPLICATION_NAME = "Flyway by Redgate"; + + /** + * The JDBC Driver instance to use. + */ + private Driver driver; + + /** + * The JDBC URL to use for connecting through the Driver. + */ + private final String url; + + /** + * The detected type of the driver. + */ + private final DriverType type; + + /** + * The JDBC user to use for connecting through the Driver. + */ + private final String user; + + /** + * The JDBC password to use for connecting through the Driver. + */ + private final String password; + + /** + * The properties to be passed to a new connection. + */ + private final Properties defaultProps; + + /** + * The ClassLoader to use. + */ + private final ClassLoader classLoader; + + /** + * Whether connection should have auto commit activated or not. Default: {@code true} + */ + private boolean autoCommit = true; + + /** + * Creates a new DriverDataSource. + * + * @param classLoader The ClassLoader to use. + * @param driverClass The name of the JDBC Driver class to use. {@code null} for url-based autodetection. + * @param url The JDBC URL to use for connecting through the Driver. (required) + * @param user The JDBC user to use for connecting through the Driver. + * @param password The JDBC password to use for connecting through the Driver. + * @throws FlywayException when the datasource could not be created. + */ + public DriverDataSource(ClassLoader classLoader, String driverClass, String url, String user, String password) throws FlywayException { + this(classLoader, driverClass, url, user, password, new Properties()); + } + + /** + * Creates a new DriverDataSource. + * + * @param classLoader The ClassLoader to use. + * @param driverClass The name of the JDBC Driver class to use. {@code null} for url-based autodetection. + * @param url The JDBC URL to use for connecting through the Driver. (required) + * @param user The JDBC user to use for connecting through the Driver. + * @param password The JDBC password to use for connecting through the Driver. + * @param props The properties to pass to the connection. + * @throws FlywayException when the datasource could not be created. + */ + public DriverDataSource(ClassLoader classLoader, String driverClass, String url, String user, String password, + Properties props) throws FlywayException { + this.classLoader = classLoader; + this.url = detectFallbackUrl(url); + this.type = detectDriverTypeForUrl(url); + + if (!StringUtils.hasLength(driverClass)) { + if (type == null) { + throw new FlywayException("Unable to autodetect JDBC driver for url: " + url); + } + + driverClass = detectDriverForType(type); + } + + this.defaultProps = new Properties(props); + this.defaultProps.putAll(detectPropsForType(type)); + + try { + this.driver = ClassUtils.instantiate(driverClass, classLoader); + } catch (FlywayException e) { + String backupDriverClass = detectBackupDriverForType(type); + if (backupDriverClass == null) { + throw new FlywayException("Unable to instantiate JDBC driver: " + driverClass + + " => Check whether the jar file is present", e, + ErrorCode.JDBC_DRIVER); + } + try { + this.driver = ClassUtils.instantiate(backupDriverClass, classLoader); + } catch (Exception e1) { + // Only report original exception about primary driver + throw new FlywayException( + "Unable to instantiate JDBC driver: " + driverClass + " => Check whether the jar file is present", e, + ErrorCode.JDBC_DRIVER); + } + } + + this.user = detectFallbackUser(user); + this.password = detectFallbackPassword(password); + } + + /** + * Detects a fallback url in case this one is missing. + * + * @param url The url to check. + * @return The url to use. + */ + private String detectFallbackUrl(String url) { + if (!StringUtils.hasText(url)) { + // Attempt fallback to the automatically provided Boxfuse database URL (https://boxfuse.com/docs/databases#envvars) + String boxfuseDatabaseUrl = System.getenv("BOXFUSE_DATABASE_URL"); + if (StringUtils.hasText(boxfuseDatabaseUrl)) { + return boxfuseDatabaseUrl; + } + + throw new FlywayException("Missing required JDBC URL. Unable to create DataSource!"); + } + + if (!url.toLowerCase().startsWith("jdbc:")) { + throw new FlywayException("Invalid JDBC URL (should start with jdbc:) : " + url); + } + return url; + } + + /** + * Detects a fallback user in case this one is missing. + * + * @param user The user to check. + * @return The user to use. + */ + private String detectFallbackUser(String user) { + if (!StringUtils.hasText(user)) { + // Attempt fallback to the automatically provided Boxfuse database user (https://boxfuse.com/docs/databases#envvars) + String boxfuseDatabaseUser = System.getenv("BOXFUSE_DATABASE_USER"); + if (StringUtils.hasText(boxfuseDatabaseUser)) { + return boxfuseDatabaseUser; + } + } + return user; + } + + /** + * Detects whether a user is required from configuration. This may not be the case if the driver supports + * other authentication mechanisms, or supports the user being encoded in the URL + * + * @param url The url to check + * @return false if a username needs to be provided + */ + public static boolean detectUserRequiredByUrl(String url) { + // Using Snowflake private-key auth instead of password allows user to be passed on URL + if (DriverDataSource.DriverType.SNOWFLAKE.matches(url) + || DriverDataSource.DriverType.POSTGRESQL.matches(url)) { + return !url.contains("user="); + } + if (DriverDataSource.DriverType.SQLSERVER.matches(url)) { + return !url.contains("integratedSecurity=") + && !url.contains("authentication=ActiveDirectoryIntegrated") + && !url.contains("authentication=ActiveDirectoryMSI"); + } + if (DriverDataSource.DriverType.ORACLE.matches(url)) { + // Oracle usernames/passwords can be 1-30 chars, can only contain alphanumerics and # _ $ + Pattern pattern = Pattern.compile("^jdbc:oracle:thin:[a-zA-Z0-9#_$]+/[a-zA-Z0-9#_$]+@//.*"); + return !pattern.matcher(url).matches(); + } + return true; + } + + /** + * Detects whether a password is required from configuration. This may not be the case if the driver supports + * other authentication mechanisms, or supports the password being encoded in the URL + * + * @param url The url to check + * @return false if a username needs to be provided + */ + public static boolean detectPasswordRequiredByUrl(String url) { + // Using Snowflake private-key auth instead of password + if (DriverDataSource.DriverType.SNOWFLAKE.matches(url)) { + return !url.contains("private_key_file="); + } + // Postgres supports password in URL + if (DriverDataSource.DriverType.POSTGRESQL.matches(url)) { + return !url.contains("password="); + } + if (DriverDataSource.DriverType.SQLSERVER.matches(url)) { + return !url.contains("integratedSecurity=") + && !url.contains("authentication=ActiveDirectoryIntegrated") + && ! url.contains("authentication=ActiveDirectoryMSI"); + } + if (DriverDataSource.DriverType.ORACLE.matches(url)) { + // Oracle usernames/passwords can be 1-30 chars, can only contain alphanumerics and # _ $ + Pattern pattern = Pattern.compile("^jdbc:oracle:thin:[a-zA-Z0-9#_$]+/[a-zA-Z0-9#_$]+@//.*"); + return !pattern.matcher(url).matches(); + } + return true; + } + + /** + * Detects a fallback password in case this one is missing. + * + * @param password The password to check. + * @return The password to use. + */ + private String detectFallbackPassword(String password) { + if (!StringUtils.hasText(password)) { + // Attempt fallback to the automatically provided Boxfuse database password (https://boxfuse.com/docs/databases#envvars) + String boxfuseDatabasePassword = System.getenv("BOXFUSE_DATABASE_PASSWORD"); + if (StringUtils.hasText(boxfuseDatabasePassword)) { + return boxfuseDatabasePassword; + } + } + return password; + } + + /** + * Detect the default connection properties for this driver type. + * + * @param type The driver type. + * @return The properties. + */ + private Properties detectPropsForType(DriverType type) { + Properties result = new Properties(); + + if (DriverType.ORACLE.equals(type)) { + String osUser = System.getProperty("user.name"); + result.put("v$session.osuser", osUser.substring(0, Math.min(osUser.length(), 30))); + result.put("v$session.program", APPLICATION_NAME); + result.put("oracle.net.keepAlive", "true"); + String oobb = ClassUtils.getStaticFieldValue("oracle.jdbc.OracleConnection", "CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK", classLoader); + result.put(oobb, "true"); + } else if (DriverType.SQLSERVER.equals(type)) { + result.put("applicationName", APPLICATION_NAME); + } else if (DriverType.POSTGRESQL.equals(type)) { + result.put("ApplicationName", APPLICATION_NAME); + } else if (DriverType.MYSQL.equals(type) || DriverType.MARIADB.equals(type)) { + result.put("connectionAttributes", "program_name:" + APPLICATION_NAME); + } else if (DriverType.DB2.equals(type)) { + result.put("clientProgramName", APPLICATION_NAME); + result.put("retrieveMessagesFromServerOnGetMessage", "true"); + } else if (DriverType.SYBASE.equals(type)) { + result.put("APPLICATIONNAME", APPLICATION_NAME); + } else if (DriverType.SAPHANA.equals(type)) { + result.put("SESSIONVARIABLE:APPLICATION", APPLICATION_NAME); + } else if (DriverType.FIREBIRD_SQL.equals(type) || DriverType.FIREBIRD.equals(type)) { + result.put("processName", APPLICATION_NAME); + } + + + return result; + } + + /** + * Detects the driver type for the url by checking the start of the url against the DriverType prefixes + * @param url The url to check + * @return The detected driver type + */ + private DriverType detectDriverTypeForUrl(String url) { + for (DriverType type : DriverType.values()) { + if (type.matches(url)) { + return type; + } + } + + return null; + } + + /** + * Retrieves a second choice backup driver for a given driver type, in case the primary driver is not available. + * + * @param type The detected driver type. + * @return The JDBC driver. {@code null} if none. + */ + private String detectBackupDriverForType(DriverType type) { + if (DriverType.MYSQL.equals(type) && ClassUtils.isPresent(MYSQL_LEGACY_JDBC_DRIVER, classLoader)) { + return MYSQL_LEGACY_JDBC_DRIVER; + } + + if (DriverType.MYSQL.equals(type) && ClassUtils.isPresent(DriverType.MARIADB.driverClass, classLoader)) { + LOG.warn("You are attempting to connect to a MySQL database using the MariaDB driver." + + " This is known to cause issues." + + " An upgrade to Oracle's MySQL JDBC driver is highly recommended."); + return DriverType.MARIADB.driverClass; + } + + if (DriverType.REDSHIFT.equals(type)) { + if (ClassUtils.isPresent(REDSHIFT_JDBC41_DRIVER, classLoader)) { + return REDSHIFT_JDBC41_DRIVER; + } + return REDSHIFT_JDBC4_DRIVER; + } + + return null; + } + + /** + * Detects the correct Jdbc driver for this driver type. + * + * @param type The detected driver type. + * @return The Jdbc driver. + */ + private String detectDriverForType(DriverType type) { + if (DriverType.SQLLITE.equals(type)) { + if (new FeatureDetector(classLoader).isAndroidAvailable()) { + return DriverType.SQLDROID.driverClass; + } + } + + return type.driverClass; + } + + /** + * @return the JDBC Driver instance to use. + */ + public Driver getDriver() { + return this.driver; + } + + /** + * @return the JDBC URL to use for connecting through the Driver. + */ + public String getUrl() { + return this.url; + } + + /** + * @return the JDBC user to use for connecting through the Driver. + */ + public String getUser() { + return this.user; + } + + /** + * @return the JDBC password to use for connecting through the Driver. + */ + public String getPassword() { + return this.password; + } + + /** + * This implementation delegates to {@code getConnectionFromDriver}, + * using the default user and password of this DataSource. + * + * @see #getConnectionFromDriver(String, String) + */ + @Override + public Connection getConnection() throws SQLException { + return getConnectionFromDriver(getUser(), getPassword()); + } + + /** + * This implementation delegates to {@code getConnectionFromDriver}, + * using the given user and password. + * + * @see #getConnectionFromDriver(String, String) + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return getConnectionFromDriver(username, password); + } + + + /** + * Build properties for the Driver, including the given user and password (if any), + * and obtain a corresponding Connection. + * + * @param username the name of the user + * @param password the password to use + * @return the obtained Connection + * @throws SQLException in case of failure + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + protected Connection getConnectionFromDriver(String username, String password) throws SQLException { + Properties props = new Properties(this.defaultProps); + if (username != null) { + props.setProperty("user", username); + } + if (password != null) { + props.setProperty("password", password); + } + + Connection connection = driver.connect(url, props); + if (connection == null) { + throw new FlywayException("Unable to connect to " + url); + } + connection.setAutoCommit(autoCommit); + return connection; + } + + /** + * @return Whether connection should have auto commit activated or not. Default: {@code true} + */ + public boolean isAutoCommit() { + return autoCommit; + } + + /** + * @param autoCommit Whether connection should have auto commit activated or not. Default: {@code true} + */ + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public void setLoginTimeout(int timeout) { + unsupportedMethod("setLoginTimeout"); + } + + @Override + public PrintWriter getLogWriter() { + unsupportedMethod("getLogWriter"); + return null; + } + + @Override + public void setLogWriter(PrintWriter pw) { + unsupportedMethod("setLogWriter"); + } + + @Override + public T unwrap(Class iface) { + unsupportedMethod("unwrap"); + return null; + } + + @Override + public boolean isWrapperFor(Class iface) { + return DataSource.class.equals(iface); + } + + @Override + public Logger getParentLogger() { + unsupportedMethod("getParentLogger"); + return null; + } + + private void unsupportedMethod(String methodName) { + throw new UnsupportedOperationException(methodName); + } + + /** + * Shutdown the database that was opened (only applicable to embedded databases that require this). + */ + public void shutdownDatabase() { + if (DriverType.DERBY_EMBEDDED.equals(type)) { + try { + int i = url.indexOf(";"); + String shutdownUrl = (i < 0 ? url : url.substring(0, i)) + ";shutdown=true"; + + driver.connect(shutdownUrl, new Properties()); + } catch (SQLException e) { + LOG.debug("Expected error on Derby Embedded Database shutdown: " + e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ErrorImpl.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ErrorImpl.java new file mode 100644 index 00000000..581a44af --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ErrorImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.callback.Error; + +public class ErrorImpl implements Error { + private final int code; + private final String state; + private final String message; + private boolean handled; + + /** + * An error that occurred while executing a statement. + * + * @param code The error code. + * @param state The error state. + * @param message The error message. + */ + public ErrorImpl(int code, String state, String message) { + this.code = code; + this.state = state; + this.message = message; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getState() { + return state; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public boolean isHandled() { + return handled; + } + + @Override + public void setHandled(boolean handled) { + this.handled = handled; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplate.java new file mode 100644 index 00000000..d2ce1310 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplate.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing operations in the context of a database connection. + */ +public interface ExecutionTemplate { + + /** + * Executes this callback within the context of the connection + * + * @param callback The callback to execute. + * @return The result of the callback. + */ + T execute(Callable callback); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplateFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplateFactory.java new file mode 100644 index 00000000..aaadb330 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplateFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; + +import java.sql.Connection; + +public class ExecutionTemplateFactory { + /** + * Creates a new execution template for this connection. + * If possible, will attempt to roll back when an exception is thrown. + * + * @param connection The connection for execution. + */ + public static ExecutionTemplate createExecutionTemplate(Connection connection) { + return createTransactionalExecutionTemplate(connection, true); + } + + /** + * Creates a new execution template for this connection. + * If possible, will attempt to roll back when an exception is thrown. + * + * @param connection The connection for execution. + * @param database The database + */ + public static ExecutionTemplate createExecutionTemplate(Connection connection, Database database) { + if (database.supportsMultiStatementTransactions()) { + return createTransactionalExecutionTemplate(connection, true); + } + + return new PlainExecutionTemplate(); + } + + /** + * Creates a new execution template for this connection, which attempts to get exclusive access to the table + * + * @param connection The connection for execution. + * @param database The database + */ + public static ExecutionTemplate createTableExclusiveExecutionTemplate(Connection connection, Table table, Database database) { + if (database.supportsMultiStatementTransactions()) { + return new TableLockingExecutionTemplate(table, createTransactionalExecutionTemplate(connection, database.supportsDdlTransactions())); + } + + return new TableLockingExecutionTemplate(table, new PlainExecutionTemplate()); + } + + /** + * Creates a new transactional execution template for this connection. + * + * @param connection The connection for execution. + * @param rollbackOnException Whether to attempt to roll back when an exception is thrown. + */ + private static ExecutionTemplate createTransactionalExecutionTemplate(Connection connection, boolean rollbackOnException) { + DatabaseType databaseType = DatabaseType.fromJdbcConnection(connection); + + if (DatabaseType.COCKROACHDB.equals(databaseType)) { + return new CockroachRetryingTransactionalExecutionTemplate(connection, rollbackOnException); + } + + return new TransactionalExecutionTemplate(connection, rollbackOnException); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcConnectionFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcConnectionFactory.java new file mode 100644 index 00000000..ddc87539 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcConnectionFactory.java @@ -0,0 +1,253 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; + + +import org.flywaydb.core.internal.util.ExceptionUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * Utility class for dealing with jdbc connections. + */ +public class JdbcConnectionFactory { + private static final Log LOG = LogFactory.getLog(JdbcConnectionFactory.class); + + private final DataSource dataSource; + private final int connectRetries; + private final DatabaseType databaseType; + private final String jdbcUrl; + private final String driverInfo; + private final String productName; + + private Connection firstConnection; + private ConnectionInitializer connectionInitializer; + + + + + + + + + + + + + + + + + /** + * Creates a new JDBC connection factory. This automatically opens a first connection which can be obtained via + * a call to getConnection and which must be closed again to avoid leaking it. + * + * @param dataSource The dataSource to obtain the connection from. + * @param connectRetries The maximum number of retries when attempting to connect to the database. + + + + */ + public JdbcConnectionFactory(DataSource dataSource, int connectRetries + + + + ) { + this.dataSource = dataSource; + this.connectRetries = connectRetries; + + firstConnection = JdbcUtils.openConnection(dataSource, connectRetries); + + this.databaseType = DatabaseType.fromJdbcConnection(firstConnection); + final DatabaseMetaData databaseMetaData = JdbcUtils.getDatabaseMetaData(firstConnection); + this.jdbcUrl = getJdbcUrl(databaseMetaData); + this.driverInfo = getDriverInfo(databaseMetaData); + this.productName = JdbcUtils.getDatabaseProductName(databaseMetaData); + + + + + + } + + public void setConnectionInitializer(ConnectionInitializer connectionInitializer) { + this.connectionInitializer = connectionInitializer; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /** + * @return The type of database this is. + */ + public DatabaseType getDatabaseType() { + return databaseType; + } + + /** + * @return The JDBC url for these connections. + */ + public String getJdbcUrl() { + return jdbcUrl; + } + + public String getDriverInfo() { + return driverInfo; + } + + public String getProductName() { + return productName; + } + + /** + * Opens a new connection from this dataSource. + * + * @return The new connection. + * @throws FlywayException when the connection could not be opened. + */ + public Connection openConnection() throws FlywayException { + Connection connection = + firstConnection == null ? JdbcUtils.openConnection(dataSource, connectRetries) : firstConnection; + firstConnection = null; + + if (connectionInitializer != null) { + connectionInitializer.initialize(this, connection); + } + + + + + + + + + + + + + return connection; + } + + public interface ConnectionInitializer { + void initialize(JdbcConnectionFactory jdbcConnectionFactory, Connection connection); + } + + /** + * Retrieves the Jdbc Url for this connection. + * + * @param databaseMetaData The Jdbc connection metadata. + * @return The Jdbc Url. + */ + + private static String getJdbcUrl(DatabaseMetaData databaseMetaData) { + String url; + try { + url = databaseMetaData.getURL(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to retrieve the JDBC connection URL!", e); + } + if (url == null) { + return ""; + } + return filterUrl(url); + } + + /** + * Filter out parameters to avoid including passwords, etc. + * + * @param url The raw url. + * @return The filtered url. + */ + static String filterUrl(String url) { + int questionMark = url.indexOf("?"); + if (questionMark >= 0 && !url.contains("?databaseName=")) { + url = url.substring(0, questionMark); + } + url = url.replaceAll("://.*:.*@", "://"); + return url; + } + + private static String getDriverInfo(DatabaseMetaData databaseMetaData) { + try { + return databaseMetaData.getDriverName() + " " + databaseMetaData.getDriverVersion(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to read database driver info: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcTemplate.java new file mode 100644 index 00000000..e1e47005 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcTemplate.java @@ -0,0 +1,428 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import java.sql.BatchUpdateException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Collection of utility methods for querying the DB. Inspired by Spring's JdbcTemplate. + */ +public class JdbcTemplate { + /** + * The DB connection to use. + */ + private final Connection connection; + + /** + * The type to assign to a null value. + */ + private final int nullType; + + /** + * Creates a new JdbcTemplate. + * + * @param connection The database connection to use. + */ + public JdbcTemplate(Connection connection) { + this(connection, DatabaseType.fromJdbcConnection(connection)); + } + + /** + * Creates a new JdbcTemplate. + * + * @param connection The database connection to use. + */ + public JdbcTemplate(Connection connection, DatabaseType databaseType) { + this.connection = connection; + this.nullType = databaseType.getNullType(); + } + + /** + * @return The DB connection to use. + */ + public Connection getConnection() { + return connection; + } + + /** + * Executes this query with these parameters against this connection. + * + * @param query The query to execute. + * @param params The query parameters. + * @return The query results. + * @throws SQLException when the query execution failed. + */ + public List> queryForList(String query, Object... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + List> result; + try { + statement = prepareStatement(query, params); + resultSet = statement.executeQuery(); + + result = new ArrayList<>(); + while (resultSet.next()) { + Map rowMap = new LinkedHashMap<>(); + for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) { + rowMap.put(resultSet.getMetaData().getColumnLabel(i), resultSet.getString(i)); + } + result.add(rowMap); + } + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * Executes this query with these parameters against this connection. + * + * @param query The query to execute. + * @param params The query parameters. + * @return The query results as a list of strings. + * @throws SQLException when the query execution failed. + */ + public List queryForStringList(String query, String... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + List result; + try { + statement = prepareStatement(query, params); + resultSet = statement.executeQuery(); + + result = new ArrayList<>(); + while (resultSet.next()) { + result.add(resultSet.getString(1)); + } + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * Executes this query with these parameters against this connection. + * + * @param query The query to execute. + * @param params The query parameters. + * @return The query result. + * @throws SQLException when the query execution failed. + */ + public int queryForInt(String query, String... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + int result; + try { + statement = prepareStatement(query, params); + resultSet = statement.executeQuery(); + resultSet.next(); + result = resultSet.getInt(1); + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * Executes this query with these parameters against this connection. + * + * @param query The query to execute. + * @param params The query parameters. + * @return The query result. + * @throws SQLException when the query execution failed. + */ + public boolean queryForBoolean(String query, String... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + boolean result; + try { + statement = prepareStatement(query, params); + resultSet = statement.executeQuery(); + resultSet.next(); + result = resultSet.getBoolean(1); + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * Executes this query with these parameters against this connection. + * + * @param query The query to execute. + * @param params The query parameters. + * @return The query result. + * @throws SQLException when the query execution failed. + */ + public String queryForString(String query, String... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + String result; + try { + statement = prepareStatement(query, params); + resultSet = statement.executeQuery(); + result = null; + if (resultSet.next()) { + result = resultSet.getString(1); + } + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return result; + } + + /** + * Executes this sql statement using a PreparedStatement. + * + * @param sql The statement to execute. + * @param params The statement parameters. + * @throws SQLException when the execution failed. + */ + public void execute(String sql, Object... params) throws SQLException { + PreparedStatement statement = null; + try { + statement = prepareStatement(sql, params); + statement.execute(); + } finally { + JdbcUtils.closeStatement(statement); + } + } + + /** + * Executes this sql statement using an ordinary Statement. + * + * @param sql The statement to execute. + * @return the results of the execution. + */ + public Results executeStatement(String sql) { + Results results = new Results(); + Statement statement = null; + try { + statement = connection.createStatement(); + statement.setEscapeProcessing(false); + boolean hasResults; + try { + hasResults = statement.execute(sql); + } finally { + extractWarnings(results, statement); + } + extractResults(results, statement, sql, hasResults); + } catch (final SQLException e) { + extractErrors(results, e); + } finally { + JdbcUtils.closeStatement(statement); + } + return results; + } + + private void extractWarnings(Results results, Statement statement) throws SQLException { + SQLWarning warning = statement.getWarnings(); + while (warning != null) { + int code = warning.getErrorCode(); + String state = warning.getSQLState(); + String message = warning.getMessage(); + + if (state == null) + { + state = ""; + } + + if (message == null) + { + message = ""; + } + + results.addWarning(new WarningImpl(code, state, message)); + warning = warning.getNextWarning(); + } + } + + public void extractErrors(Results results, SQLException e) { + + + + + + + + results.setException(e); + } + + private void extractResults(Results results, Statement statement, String sql, boolean hasResults) throws SQLException { + // retrieve all results to ensure all errors are detected + int updateCount = -1; + while (hasResults || (updateCount = statement.getUpdateCount()) != -1) { + List columns = null; + List> data = null; + if (hasResults) { + try (ResultSet resultSet = statement.getResultSet()) { + columns = new ArrayList<>(); + ResultSetMetaData metadata = resultSet.getMetaData(); + int columnCount = metadata.getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + columns.add(metadata.getColumnName(i)); + } + + data = new ArrayList<>(); + while (resultSet.next()) { + List row = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + row.add(resultSet.getString(i)); + } + data.add(row); + } + } + } + results.addResult(new Result(updateCount, columns, data, sql)); + hasResults = statement.getMoreResults(); + } + } + + /** + * Executes this update sql statement. + * + * @param sql The statement to execute. + * @param params The statement parameters. + * @throws SQLException when the execution failed. + */ + public void update(String sql, Object... params) throws SQLException { + PreparedStatement statement = null; + try { + statement = prepareStatement(sql, params); + statement.executeUpdate(); + } finally { + JdbcUtils.closeStatement(statement); + } + } + + /** + * Creates a new prepared statement for this sql with these params. + * + * @param sql The sql to execute. + * @param params The params. + * @return The new prepared statement. + * @throws SQLException when the statement could not be prepared. + */ + private PreparedStatement prepareStatement(String sql, Object[] params) throws SQLException { + PreparedStatement statement = connection.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + if (params[i] == null) { + statement.setNull(i + 1, nullType); + } else if (params[i] instanceof Integer) { + statement.setInt(i + 1, (Integer) params[i]); + } else if (params[i] instanceof Boolean) { + statement.setBoolean(i + 1, (Boolean) params[i]); + } else { + statement.setString(i + 1, params[i].toString()); + } + } + return statement; + } + + /** + * Executes this query and map the results using this row mapper. + * + * @param sql The query to execute. + * @param rowMapper The row mapper to use. + * @param The type of the result objects. + * @return The list of results. + * @throws SQLException when the query failed to execute. + */ + public List query(String sql, RowMapper rowMapper, Object... params) throws SQLException { + PreparedStatement statement = null; + ResultSet resultSet = null; + + List results; + try { + statement = prepareStatement(sql, params); + resultSet = statement.executeQuery(); + + results = new ArrayList<>(); + while (resultSet.next()) { + results.add(rowMapper.mapRow(resultSet)); + } + } finally { + JdbcUtils.closeResultSet(resultSet); + JdbcUtils.closeStatement(statement); + } + + return results; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcUtils.java new file mode 100644 index 00000000..4ca35790 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcUtils.java @@ -0,0 +1,210 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.util.ExceptionUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Utility class for dealing with jdbc connections. + */ +public class JdbcUtils { + private static final Log LOG = LogFactory.getLog(JdbcUtils.class); + + /** + * Prevents instantiation. + */ + private JdbcUtils() { + //Do nothing + } + + /** + * Opens a new connection from this dataSource. + * + * @param dataSource The dataSource to obtain the connection from. + * @param connectRetries The maximum number of retries when attempting to connect to the database. + * @return The new connection. + * @throws FlywayException when the connection could not be opened. + */ + public static Connection openConnection(DataSource dataSource, int connectRetries) throws FlywayException { + int retries = 0; + while (true) { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + if ("08S01".equals(e.getSQLState()) && e.getMessage().contains("This driver is not configured for integrated authentication")) { + throw new FlywaySqlException("Unable to obtain connection from database" + + getDataSourceInfo(dataSource) + ": " + e.getMessage() + "\nTo setup integrated authentication see https://flywaydb.org/documentation/database/sqlserver#windows-authentication--azure-active-directory", e); + } + + if (++retries > connectRetries) { + throw new FlywaySqlException("Unable to obtain connection from database" + + getDataSourceInfo(dataSource) + ": " + e.getMessage(), e); + } + Throwable rootCause = ExceptionUtils.getRootCause(e); + String msg = "Connection error: " + e.getMessage(); + if (rootCause != null && rootCause != e && rootCause.getMessage() != null) { + msg += " (Caused by " + rootCause.getMessage() + ")"; + } + LOG.warn(msg + " Retrying in 1 sec..."); + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + throw new FlywaySqlException("Unable to obtain connection from database" + + getDataSourceInfo(dataSource) + ": " + e.getMessage(), e); + } + } + } + } + + private static String getDataSourceInfo(DataSource dataSource) { + if (!(dataSource instanceof DriverDataSource)) { + return ""; + } + DriverDataSource driverDataSource = (DriverDataSource) dataSource; + return " (" + driverDataSource.getUrl() + ") for user '" + driverDataSource.getUser() + "'"; + } + + /** + * Safely closes this connection. This method never fails. + * + * @param connection The connection to close. + */ + public static void closeConnection(Connection connection) { + if (connection == null) { + return; + } + + try { + connection.close(); + } catch (Exception e) { + LOG.error("Error while closing database connection: " + e.getMessage(), e); + } + } + + /** + * Safely closes this statement. This method never fails. + * + * @param statement The statement to close. + */ + public static void closeStatement(Statement statement) { + if (statement == null) { + return; + } + + try { + statement.close(); + } catch (SQLException e) { + LOG.error("Error while closing JDBC statement", e); + } + } + + /** + * Safely closes this resultSet. This method never fails. + * + * @param resultSet The resultSet to close. + */ + public static void closeResultSet(ResultSet resultSet) { + if (resultSet == null) { + return; + } + + try { + resultSet.close(); + } catch (SQLException e) { + LOG.error("Error while closing JDBC resultSet", e); + } + } + + /** + * Retrieves the database metadata for this connection. + * + * @param connection The connection to use to query the database. + * @return The database metadata. + */ + public static DatabaseMetaData getDatabaseMetaData(Connection connection) { + DatabaseMetaData databaseMetaData; + try { + databaseMetaData = connection.getMetaData(); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to read database connection metadata: " + e.getMessage(), e); + } + if (databaseMetaData == null) { + throw new FlywayException("Unable to read database connection metadata while it is null!"); + } + return databaseMetaData; + } + + /** + * Retrieves the name of the database product. + * + * @param databaseMetaData The connection metadata to use to query the database. + * @return The name of the database product. Ex.: Oracle, MySQL, ... + */ + public static String getDatabaseProductName(DatabaseMetaData databaseMetaData) { + try { + String databaseProductName = databaseMetaData.getDatabaseProductName(); + if (databaseProductName == null) { + throw new FlywayException("Unable to determine database. Product name is null."); + } + + int databaseMajorVersion = databaseMetaData.getDatabaseMajorVersion(); + int databaseMinorVersion = databaseMetaData.getDatabaseMinorVersion(); + + return databaseProductName + " " + databaseMajorVersion + "." + databaseMinorVersion; + } catch (SQLException e) { + throw new FlywaySqlException("Error while determining database product name", e); + } + } + + /** + * Retrieves the version of the database product. + * + * @param databaseMetaData The connection metadata to use to query the database. + * @return The version of the database product. Ex.: MariaDB 10.3, ... + */ + public static String getDatabaseProductVersion(DatabaseMetaData databaseMetaData) { + try { + return databaseMetaData.getDatabaseProductVersion(); + } catch (SQLException e) { + throw new FlywaySqlException("Error while determining database product version", e); + } + } + + /** + * Retrieves the name of the database driver. + * + * @param databaseMetaData The connection metadata to use to query the database. + * @return The name of the database driver. Ex.: MariaDB JDBC driver, ... + */ + public static String getDriverName(DatabaseMetaData databaseMetaData) { + try { + return databaseMetaData.getDriverName(); + } catch (SQLException e) { + throw new FlywaySqlException("Error while determining database driver name", e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/PlainExecutionTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/PlainExecutionTemplate.java new file mode 100644 index 00000000..d4e7236e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/PlainExecutionTemplate.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; + +import java.sql.SQLException; +import java.util.concurrent.Callable; + +public class PlainExecutionTemplate implements ExecutionTemplate { + private static final Log LOG = LogFactory.getLog(PlainExecutionTemplate.class); + + @Override + public T execute(Callable callback) { + try { + LOG.debug("Performing operation in non-transactional context."); + return callback.call(); + } catch (Exception e) { + LOG.error("Failed to execute operation in non-transactional context. Please restore backups and roll back database and code!"); + + if (e instanceof SQLException) { + throw new FlywaySqlException("Failed to execute operation.", (SQLException) e); + } + + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } + + throw new FlywayException(e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Result.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Result.java new file mode 100644 index 00000000..8edccf92 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Result.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import java.util.List; + +public class Result { + private final long updateCount; + private final List columns; + private final List> data; + private final String sql; + + public Result(long updateCount, List columns, List> data, String sql) { + this.updateCount = updateCount; + this.columns = columns; + this.data = data; + this.sql = sql; + } + + public long getUpdateCount() { + return updateCount; + } + + public List getColumns() { + return columns; + } + + public List> getData() { + return data; + } + + public String getSql() { + return sql; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Results.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Results.java new file mode 100644 index 00000000..8be3db3b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Results.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Warning; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Container for all results, warnings, errors and remaining side-effects of a sql statement. + */ +public class Results { + public static final Results EMPTY_RESULTS = new Results(); + + private final List results = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private SQLException exception; + + public void addResult(Result result) { + results.add(result); + } + + public void addWarning(Warning warning) { + warnings.add(warning); + } + + public void addError(Error error) { + errors.add(error); + } + + public List getWarnings() { + return warnings; + } + + public List getErrors() { + return errors; + } + + public List getResults() { + return results; + } + + public SQLException getException() { + return exception; + } + + public void setException(SQLException exception) { + this.exception = exception; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/RowMapper.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/RowMapper.java new file mode 100644 index 00000000..014ef2eb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/RowMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Mapper from ResultSet row to object. + * + * @param The type of object to map to. + */ +public interface RowMapper { + /** + * Maps a row in this resultSet to an object. + * @param rs The resultset, already positioned on the row to map. + * @return The corresponding object. + * @throws SQLException when reading the resultset failed. + */ + T mapRow(final ResultSet rs) throws SQLException; +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TableLockingExecutionTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TableLockingExecutionTemplate.java new file mode 100644 index 00000000..826ce98b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TableLockingExecutionTemplate.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.internal.database.base.Table; + +import java.util.concurrent.Callable; + +public class TableLockingExecutionTemplate implements ExecutionTemplate { + private final Table table; + private final ExecutionTemplate executionTemplate; + + TableLockingExecutionTemplate(Table table, ExecutionTemplate executionTemplate) { + this.table = table; + this.executionTemplate = executionTemplate; + } + + @Override + public T execute(final Callable callback) { + return executionTemplate.execute(new Callable() { + @Override + public T call() throws Exception { + try { + table.lock(); + return callback.call(); + } finally { + table.unlock(); + } + } + }); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TransactionalExecutionTemplate.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TransactionalExecutionTemplate.java new file mode 100644 index 00000000..ff62f10a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TransactionalExecutionTemplate.java @@ -0,0 +1,103 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.exception.FlywaySqlException; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * Spring-like template for executing transactions. + */ +public class TransactionalExecutionTemplate implements ExecutionTemplate { + private static final Log LOG = LogFactory.getLog(TransactionalExecutionTemplate.class); + + /** + * The connection to the database + */ + private final Connection connection; + + /** + * Whether to roll back the transaction when an exception is thrown. + */ + private final boolean rollbackOnException; + + /** + * Creates a new transaction template for this connection. + * + * @param connection The connection for the transaction. + * @param rollbackOnException Whether to roll back the transaction when an exception is thrown. + */ + TransactionalExecutionTemplate(Connection connection, boolean rollbackOnException) { + this.connection = connection; + this.rollbackOnException = rollbackOnException; + } + + /** + * Executes this callback within a transaction. + * + * @param callback The callback to execute. + * @return The result of the transaction code. + */ + @Override + public T execute(Callable callback) { + boolean oldAutocommit = true; + try { + oldAutocommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + T result = callback.call(); + connection.commit(); + return result; + } catch (Exception e) { + RuntimeException rethrow; + if (e instanceof SQLException) { + rethrow = new FlywaySqlException("Unable to commit transaction", (SQLException) e); + } else if (e instanceof RuntimeException) { + rethrow = (RuntimeException) e; + } else { + rethrow = new FlywayException(e); + } + + if (rollbackOnException) { + try { + LOG.debug("Rolling back transaction..."); + connection.rollback(); + LOG.debug("Transaction rolled back"); + } catch (SQLException se) { + LOG.error("Unable to rollback transaction", se); + } + } else { + try { + connection.commit(); + } catch (SQLException se) { + LOG.error("Unable to commit transaction", se); + } + } + throw rethrow; + } finally { + try { + connection.setAutoCommit(oldAutocommit); + } catch (SQLException e) { + LOG.error("Unable to restore autocommit to original value for connection", e); + } + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/WarningImpl.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/WarningImpl.java new file mode 100644 index 00000000..9fc9393c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/WarningImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.jdbc; + +import org.flywaydb.core.api.callback.Warning; + +public class WarningImpl implements Warning { + private final int code; + private final String state; + private final String message; + private boolean handled; + + /** + * An warning that occurred while executing a statement. + * @param code The warning code. + * @param state The warning state. + * @param message The warning message. + */ + public WarningImpl(int code, String state, String message) { + this.code = code; + this.state = state; + this.message = message; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getState() { + return state; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public boolean isHandled() { + return handled; + } + + @Override + public void setHandled(boolean handled) { + this.handled = handled; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/package-info.java new file mode 100644 index 00000000..05426f8b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.jdbc; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/license/Edition.java b/flyway-core/src/main/java/org/flywaydb/core/internal/license/Edition.java new file mode 100644 index 00000000..f7b7d36e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/license/Edition.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.license; + +/** + * The various editions of Flyway. + */ +public enum Edition { + COMMUNITY("Community"), + PRO("Pro"), + ENTERPRISE("Enterprise") + + + + ; + + private final String description; + + Edition(String name) { + this.description = "Flyway " + name + " Edition"; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return description; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEditionUpgradeRequiredException.java b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEditionUpgradeRequiredException.java new file mode 100644 index 00000000..206a6d90 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEditionUpgradeRequiredException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.license; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.jdbc.DatabaseType; + +/** + * Thrown when an attempt was made to migrate an older database version no longer supported by this Flyway edition. + */ +public class FlywayEditionUpgradeRequiredException extends FlywayException { + public FlywayEditionUpgradeRequiredException(Edition edition, DatabaseType databaseType, String version) { + super(edition + " or " + databaseType + " upgrade required: " + databaseType + " " + version + + " is no longer supported by " + VersionPrinter.EDITION + "," + + " but still supported by " + edition + "."); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayProUpgradeRequiredException.java b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayProUpgradeRequiredException.java new file mode 100644 index 00000000..f08aed35 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayProUpgradeRequiredException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.license; + +import org.flywaydb.core.api.FlywayException; + +/** + * Thrown when an attempt was made to use a Flyway Pro or Flyway Enterprise Edition feature not supported by + * Flyway Community Edition. + */ +public class FlywayProUpgradeRequiredException extends FlywayException { + public FlywayProUpgradeRequiredException(String feature) { + super(Edition.PRO + " or " + Edition.ENTERPRISE + " upgrade required: " + feature + + " is not supported by " + Edition.COMMUNITY + "."); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/license/VersionPrinter.java b/flyway-core/src/main/java/org/flywaydb/core/internal/license/VersionPrinter.java new file mode 100644 index 00000000..59b3db1b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/license/VersionPrinter.java @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.license; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.DateUtils; +import org.flywaydb.core.internal.util.FileCopyUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +/** + * Prints the Flyway version. + */ +public class VersionPrinter { + private static final Log LOG = LogFactory.getLog(VersionPrinter.class); + private static final String version = readVersion(); + private static boolean printed; + + public static final Edition EDITION = + + Edition.COMMUNITY + + + + + + + + + + + ; + + /** + * Prevents instantiation. + */ + private VersionPrinter() { + // Do nothing. + } + + public static String getVersion() { + return version; + } + + /** + * Prints the Flyway version. + */ + public static void printVersion( + + + + ) { + if (printed) { + return; + } + printed = true; + + + printVersionOnly(); + + + + + + + + + + + + + } + + public static void printVersionOnly() { + LOG.info(EDITION + " " + version + " by Redgate"); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + private static String readVersion() { + try { + return FileCopyUtils.copyToString( + VersionPrinter.class.getClassLoader().getResourceAsStream("org/flywaydb/core/internal/version.txt"), + StandardCharsets.UTF_8); + } catch (IOException e) { + throw new FlywayException("Unable to read Flyway version: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/license/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/license/package-info.java new file mode 100644 index 00000000..a86c88d6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/license/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.license; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/LogCreatorFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/LogCreatorFactory.java new file mode 100644 index 00000000..7ec5959a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/LogCreatorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging; + +import org.flywaydb.core.api.logging.LogCreator; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.FeatureDetector; +import org.flywaydb.core.internal.logging.android.AndroidLogCreator; +import org.flywaydb.core.internal.logging.apachecommons.ApacheCommonsLogCreator; +import org.flywaydb.core.internal.logging.javautil.JavaUtilLogCreator; +import org.flywaydb.core.internal.logging.slf4j.Slf4jLogCreator; + +public class LogCreatorFactory { + /** + * Prevent instantiation. + */ + private LogCreatorFactory() { + // Do nothing + } + + public static LogCreator getLogCreator(ClassLoader classLoader, LogCreator fallbackLogCreator) { + FeatureDetector featureDetector = new FeatureDetector(classLoader); + if (featureDetector.isAndroidAvailable()) { + return ClassUtils.instantiate(AndroidLogCreator.class.getName(), classLoader); + } + if (featureDetector.isSlf4jAvailable()) { + return ClassUtils.instantiate(Slf4jLogCreator.class.getName(), classLoader); + } + if (featureDetector.isApacheCommonsLoggingAvailable()) { + return ClassUtils.instantiate(ApacheCommonsLogCreator.class.getName(), classLoader); + } + if (fallbackLogCreator == null) { + return new JavaUtilLogCreator(); + } + return fallbackLogCreator; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLog.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLog.java new file mode 100644 index 00000000..78c3524b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLog.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.android; + +import org.flywaydb.core.api.logging.Log; + +/** + * Wrapper for an Android logger. + */ +public class AndroidLog implements Log { + /** + * The tag in the Android logs. + */ + private static final String TAG = "Flyway"; + + @Override + public boolean isDebugEnabled() { + return android.util.Log.isLoggable(TAG, android.util.Log.DEBUG); + } + + @Override + public void debug(String message) { + android.util.Log.d(TAG, message); + } + + @Override + public void info(String message) { + android.util.Log.i(TAG, message); + } + + @Override + public void warn(String message) { + android.util.Log.w(TAG, message); + } + + @Override + public void error(String message) { + android.util.Log.e(TAG, message); + } + + @Override + public void error(String message, Exception e) { + android.util.Log.e(TAG, message, e); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLogCreator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLogCreator.java new file mode 100644 index 00000000..c5165d1f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLogCreator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.android; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; + +/** + * Log Creator for Android. + */ +public class AndroidLogCreator implements LogCreator { + public Log createLogger(Class clazz) { + return new AndroidLog(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLog.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLog.java new file mode 100644 index 00000000..eecbdc14 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLog.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.apachecommons; + +import org.flywaydb.core.api.logging.Log; + +/** + * Wrapper for an Apache Commons Logging logger. + */ +public class ApacheCommonsLog implements Log { + /** + * Apache Commons Logging Logger. + */ + private final org.apache.commons.logging.Log logger; + + /** + * Creates a new wrapper around this logger. + * + * @param logger The original Apache Commons Logging Logger. + */ + public ApacheCommonsLog(org.apache.commons.logging.Log logger) { + this.logger = logger; + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + public void debug(String message) { + logger.debug(message); + } + + public void info(String message) { + logger.info(message); + } + + public void warn(String message) { + logger.warn(message); + } + + public void error(String message) { + logger.error(message); + } + + public void error(String message, Exception e) { + logger.error(message, e); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLogCreator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLogCreator.java new file mode 100644 index 00000000..ac8eb40b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLogCreator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.apachecommons; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.apache.commons.logging.LogFactory; + +/** + * Log Creator for Apache Commons Logging. + */ +public class ApacheCommonsLogCreator implements LogCreator { + public Log createLogger(Class clazz) { + return new ApacheCommonsLog(LogFactory.getLog(clazz)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/package-info.java new file mode 100644 index 00000000..d81774f3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.logging.apachecommons; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLog.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLog.java new file mode 100644 index 00000000..2763f582 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLog.java @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.javautil; + +import org.flywaydb.core.api.logging.Log; + +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * Wrapper for a java.util.Logger. + */ +public class JavaUtilLog implements Log { + /** + * Java Util Logger. + */ + private final Logger logger; + + /** + * Creates a new wrapper around this logger. + * + * @param logger The original java.util Logger. + */ + public JavaUtilLog(Logger logger) { + this.logger = logger; + } + + @Override + public boolean isDebugEnabled() { + return logger.isLoggable(Level.FINE); + } + + public void debug(String message) { + log(Level.FINE, message, null); + } + + public void info(String message) { + log(Level.INFO, message, null); + } + + public void warn(String message) { + log(Level.WARNING, message, null); + } + + public void error(String message) { + log(Level.SEVERE, message, null); + } + + public void error(String message, Exception e) { + log(Level.SEVERE, message, e); + } + + /** + * Log the message at the specified level with the specified exception if any. + * + * @param level The level to log at. + * @param message The message to log. + * @param e The exception, if any. + */ + private void log(Level level, String message, Exception e) { + // millis and thread are filled by the constructor + LogRecord record = new LogRecord(level, message); + record.setLoggerName(logger.getName()); + record.setThrown(e); + record.setSourceClassName(logger.getName()); + record.setSourceMethodName(getMethodName()); + logger.log(record); + } + + /** + * Computes the source method name for the log output. + */ + private String getMethodName() { + StackTraceElement[] steArray = new Throwable().getStackTrace(); + + for (StackTraceElement stackTraceElement : steArray) { + if (logger.getName().equals(stackTraceElement.getClassName())) { + return stackTraceElement.getMethodName(); + } + } + + return null; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLogCreator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLogCreator.java new file mode 100644 index 00000000..15fbfa55 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLogCreator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.javautil; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; + +import java.util.logging.Logger; + +/** + * Log Creator for java.util.logging. + */ +public class JavaUtilLogCreator implements LogCreator { + public Log createLogger(Class clazz) { + return new JavaUtilLog(Logger.getLogger(clazz.getName())); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/package-info.java new file mode 100644 index 00000000..dd33d528 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.logging.javautil; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/package-info.java new file mode 100644 index 00000000..6b5065c7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.logging; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLog.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLog.java new file mode 100644 index 00000000..31a0188b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLog.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.slf4j; + +import org.flywaydb.core.api.logging.Log; +import org.slf4j.Logger; + +/** + * Wrapper for a Slf4j logger. + */ +public class Slf4jLog implements Log { + /** + * Slf4j Logger. + */ + private final Logger logger; + + /** + * Creates a new wrapper around this logger. + * + * @param logger The original Slf4j Logger. + */ + public Slf4jLog(Logger logger) { + this.logger = logger; + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + public void debug(String message) { + logger.debug(message); + } + + public void info(String message) { + logger.info(message); + } + + public void warn(String message) { + logger.warn(message); + } + + public void error(String message) { + logger.error(message); + } + + public void error(String message, Exception e) { + logger.error(message, e); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLogCreator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLogCreator.java new file mode 100644 index 00000000..6c115178 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLogCreator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.logging.slf4j; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.slf4j.LoggerFactory; + +/** + * Log Creator for Slf4j. + */ +public class Slf4jLogCreator implements LogCreator { + public Log createLogger(Class clazz) { + return new Slf4jLog(LoggerFactory.getLogger(clazz.getName())); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/package-info.java new file mode 100644 index 00000000..d77b69e9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.logging.slf4j; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/output/ErrorOutput.java b/flyway-core/src/main/java/org/flywaydb/core/internal/output/ErrorOutput.java new file mode 100644 index 00000000..7bb81a19 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/output/ErrorOutput.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.output; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +public class ErrorOutput { + + public static class ErrorOutputItem { + public ErrorCode errorCode; + public String message; + public String stackTrace; + + ErrorOutputItem(ErrorCode errorCode, String message, String stackTrace) { + this.errorCode = errorCode; + this.message = message; + this.stackTrace = stackTrace; + } + } + + public ErrorOutputItem error; + + public ErrorOutput(ErrorCode errorCode, String message, String stackTrace) { + this.error = new ErrorOutputItem(errorCode, message, stackTrace); + } + + public static ErrorOutput fromException(Exception exception) { + String message = exception.getMessage(); + + if (exception instanceof FlywayException) { + FlywayException flywayException = (FlywayException)exception; + + return new ErrorOutput( + flywayException.getErrorCode(), + message == null ? "Error occurred" : message, + null); + } + + return new ErrorOutput( + ErrorCode.FAULT, + message == null ? "Fault occurred" : message, + getStackTrace(exception)); + } + + private static String getStackTrace(Exception exception) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream printStream; + + try { + printStream = new PrintStream(output, true, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + return ""; + } + + exception.printStackTrace(printStream); + + return new String(output.toByteArray(), StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutput.java b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutput.java new file mode 100644 index 00000000..0ecadbc8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutput.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.output; + +import java.util.List; + +public class InfoOutput { + public String flywayVersion; + public String database; + public String schemaVersion; + public String schemaName; + public List migrations; + + public InfoOutput(String flywayVersion, + String database, + String schemaVersion, + String schemaName, + List migrations) { + this.flywayVersion = flywayVersion; + this.database = database; + this.schemaVersion = schemaVersion; + this.schemaName = schemaName; + this.migrations = migrations; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java new file mode 100644 index 00000000..3cbce1d0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.output; + +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationState; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.license.VersionPrinter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class InfoOutputFactory { + public InfoOutput create(Configuration configuration, MigrationInfo[] migrationInfos, MigrationInfo current) { + String databaseName = getDatabaseName(configuration); + + Set undoableVersions = getUndoableVersions(migrationInfos); + + + + + + List migrationOutputs = new ArrayList<>(); + for (MigrationInfo migrationInfo : migrationInfos) { + migrationOutputs.add(createMigrationOutput(undoableVersions, migrationInfo)); + } + + MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion(); + MigrationVersion schemaVersionToOutput = currentSchemaVersion == null ? MigrationVersion.EMPTY : currentSchemaVersion; + String schemaVersion = schemaVersionToOutput.getVersion(); + String flywayVersion = VersionPrinter.getVersion(); + + return new InfoOutput( + flywayVersion, + databaseName, + schemaVersion, + join(", ", configuration.getSchemas()), + migrationOutputs); + } + + private String getDatabaseName(Configuration configuration) { + try { + return configuration.getDataSource().getConnection().getCatalog(); + } catch (Exception e) { + return ""; + } + } + + private MigrationOutput createMigrationOutput(Set undoableVersions, MigrationInfo migrationInfo) { + return new MigrationOutput(getCategory(migrationInfo), + migrationInfo.getVersion() != null ? migrationInfo.getVersion().getVersion() : "", + migrationInfo.getDescription(), + migrationInfo.getType() != null ? migrationInfo.getType().toString() : "", + migrationInfo.getInstalledOn() != null ? migrationInfo.getInstalledOn().toString() : "", + migrationInfo.getState().getDisplayName(), + getUndoableStatus(migrationInfo, undoableVersions), + migrationInfo.getPhysicalLocation() != null ? migrationInfo.getPhysicalLocation() : "", + migrationInfo.getInstalledBy() != null ? migrationInfo.getInstalledBy() : "", + migrationInfo.getExecutionTime() != null ? migrationInfo.getExecutionTime() : 0); + } + + private String join(String joiner, String[] strings) { + String output = ""; + + if (strings.length == 1) { + return strings[0]; + } + + for(String s : strings) { + output += s + joiner; + } + + return output; + } + + + private static String getUndoableStatus(MigrationInfo migrationInfo, Set undoableVersions) { + + + + + + + + + + + + + return ""; + } + + private static Set getUndoableVersions(MigrationInfo[] migrationInfos) { + Set result = new HashSet<>(); + + + + + + + + return result; + } + + private static MigrationInfo[] removeAvailableUndos(MigrationInfo[] migrationInfos) { + List result = new ArrayList<>(); + + for (MigrationInfo migrationInfo : migrationInfos) { + if (!migrationInfo.getState().equals(MigrationState.AVAILABLE)) { + result.add(migrationInfo); + } + } + + return result.toArray(new MigrationInfo[0]); + } + + private String getCategory(MigrationInfo migrationInfo) { + if (migrationInfo.getType().isSynthetic()) { + return ""; + } + if (migrationInfo.getVersion() == null) { + return "Repeatable"; + } + + + + + + return "Versioned"; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/output/MigrationOutput.java b/flyway-core/src/main/java/org/flywaydb/core/internal/output/MigrationOutput.java new file mode 100644 index 00000000..8eb91992 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/output/MigrationOutput.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.output; + +public class MigrationOutput { + public String category; + public String version; + public String description; + public String type; + public String installedOn; + public String state; + public String undoable; + public String filepath; + public String installedBy; + public int executionTime; + + public MigrationOutput(String category, String version, String description, String type, String installedOn, + String state, String undoable, String filepath, String installedBy, int executionTime) { + this.category = category; + this.version = version; + this.description = description; + this.type = type; + this.installedOn = installedOn; + this.state = state; + this.undoable = undoable; + this.filepath = filepath; + this.installedBy = installedBy; + this.executionTime = executionTime; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/output/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/output/package-info.java new file mode 100644 index 00000000..3a742b51 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/output/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.output; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/package-info.java new file mode 100644 index 00000000..b321f05b --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java new file mode 100644 index 00000000..e901ca0c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java @@ -0,0 +1,733 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.Resource; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement; +import org.flywaydb.core.internal.sqlscript.SqlStatement; +import org.flywaydb.core.internal.sqlscript.SqlStatementIterator; +import org.flywaydb.core.internal.util.BomStrippingReader; +import org.flywaydb.core.internal.util.IOUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.*; +import java.util.regex.Pattern; + +/** + * The main parser all database-specific parsers derive from. + */ +public abstract class Parser { + private static final Log LOG = LogFactory.getLog(Parser.class); + + + + + + + + + private final Configuration configuration; + private final int peekDepth; + private final char identifierQuote; + private final char alternativeIdentifierQuote; + private final char alternativeStringLiteralQuote; + private final Set validKeywords; + private final ParsingContext parsingContext; + + protected Parser(Configuration configuration, ParsingContext parsingContext, int peekDepth) { + this.configuration = configuration; + this.peekDepth = peekDepth; + this.identifierQuote = getIdentifierQuote(); + this.alternativeIdentifierQuote = getAlternativeIdentifierQuote(); + this.alternativeStringLiteralQuote = getAlternativeStringLiteralQuote(); + this.validKeywords = getValidKeywords(); + this.parsingContext = parsingContext; + } + + protected Delimiter getDefaultDelimiter() { + return Delimiter.SEMICOLON; + } + + protected char getIdentifierQuote() { + return '"'; + } + + protected char getAlternativeIdentifierQuote() { + return 0; + } + + protected char getAlternativeStringLiteralQuote() { + return 0; + } + + protected Set getValidKeywords() { + return null; + } + + /** + * Parses this resource into a stream of statements. + * + * @param resource The resource to parse. + * @return The statements. + */ + public final SqlStatementIterator parse(final LoadableResource resource) { + PositionTracker tracker = new PositionTracker(); + Recorder recorder = new Recorder(); + ParserContext context = new ParserContext(getDefaultDelimiter()); + + LOG.debug("Parsing " + resource.getFilename() + " ..."); + PeekingReader peekingReader = + new PeekingReader( + new RecordingReader(recorder, + new PositionTrackingReader(tracker, + replacePlaceholders( + new BomStrippingReader( + new BufferedReader(resource.read(), 4096)))))); + + return new ParserSqlStatementIterator(peekingReader, resource, recorder, tracker, context); + } + + /** + * Configures this reader for placeholder replacement. + * + * @param reader The original reader. + * @return The new reader with placeholder replacement. + */ + protected Reader replacePlaceholders(Reader reader) { + if (configuration.isPlaceholderReplacement()) { + return PlaceholderReplacingReader.create( + configuration, + parsingContext, + reader); + } + + return reader; + } + + private SqlStatement getNextStatement(Resource resource, PeekingReader reader, Recorder recorder, PositionTracker tracker, ParserContext context) { + resetDelimiter(context); + context.setStatementType(StatementType.UNKNOWN); + + int statementLine = tracker.getLine(); + int statementCol = tracker.getCol(); + + try { + List tokens = new ArrayList<>(); + List keywords = new ArrayList<>(); + + int statementPos = -1; + recorder.start(); + + int nonCommentPartPos = -1; + int nonCommentPartLine = -1; + int nonCommentPartCol = -1; + + StatementType statementType = StatementType.UNKNOWN; + Boolean canExecuteInTransaction = null; + + + + + String simplifiedStatement = ""; + + do { + Token token = readToken(reader, tracker, context); + if (token == null) { + if (tokens.isEmpty()) { + recorder.start(); + statementLine = tracker.getLine(); + statementCol = tracker.getCol(); + simplifiedStatement = ""; + } else { + recorder.confirm(); + } + continue; + } + + TokenType tokenType = token.getType(); + if (tokenType == TokenType.NEW_DELIMITER) { + if (!tokens.isEmpty() && nonCommentPartPos >= 0) { + String sql = recorder.stop(); + throw new FlywayException("Delimiter changed inside statement at line " + statementLine + + " col " + statementCol + ": " + sql); + } + + context.setDelimiter(new Delimiter(token.getText(), false + + + + )); + tokens.clear(); + recorder.start(); + statementLine = tracker.getLine(); + statementCol = tracker.getCol(); + simplifiedStatement = ""; + continue; + } + + if (shouldDiscard(token, nonCommentPartPos >= 0)) { + tokens.clear(); + recorder.start(); + statementLine = tracker.getLine(); + statementCol = tracker.getCol(); + simplifiedStatement = ""; + continue; + } + + if (shouldAdjustBlockDepth(context, token)) { + if (tokenType == TokenType.KEYWORD) { + keywords.add(token); + } + adjustBlockDepth(context, tokens, token, reader); + } + + + int parensDepth = token.getParensDepth(); + int blockDepth = context.getBlockDepth(); + if (TokenType.EOF == tokenType + || (TokenType.DELIMITER == tokenType && parensDepth == 0 && blockDepth == 0)) { + String sql = recorder.stop(); + if (TokenType.EOF == tokenType && (sql.length() == 0 || tokens.isEmpty() || nonCommentPartPos < 0)) { + return null; + } + if (canExecuteInTransaction == null) { + canExecuteInTransaction = true; + } + + + + + + if (TokenType.EOF == tokenType && (parensDepth > 0 || blockDepth > 0)) { + throw new FlywayException("Incomplete statement at line " + statementLine + + " col " + statementCol + ": " + sql); + } + return createStatement(reader, recorder, statementPos, statementLine, statementCol, + nonCommentPartPos, nonCommentPartLine, nonCommentPartCol, + statementType, canExecuteInTransaction, context.getDelimiter(), sql + + + + ); + } + + if (nonCommentPartPos < 0 && TokenType.COMMENT != tokenType) { + nonCommentPartPos = token.getPos(); + nonCommentPartLine = token.getLine(); + nonCommentPartCol = token.getCol(); + } + if (tokens.isEmpty()) { + statementPos = token.getPos(); + statementLine = token.getLine(); + statementCol = token.getCol(); + } + tokens.add(token); + recorder.confirm(); + + if (keywords.size() <= getTransactionalDetectionCutoff() + && (tokenType == TokenType.KEYWORD + + + + ) + && parensDepth == 0 + && (statementType == StatementType.UNKNOWN || canExecuteInTransaction == null)) { + if (!simplifiedStatement.isEmpty()) { + simplifiedStatement += " "; + } + simplifiedStatement += keywordToUpperCase(token.getText()); + + if (statementType == StatementType.UNKNOWN) { + if (keywords.size() > getTransactionalDetectionCutoff()) { + statementType = StatementType.GENERIC; + } else { + statementType = detectStatementType(simplifiedStatement); + context.setStatementType(statementType); + } + adjustDelimiter(context, statementType); + } + if (canExecuteInTransaction == null) { + if (keywords.size() > getTransactionalDetectionCutoff()) { + canExecuteInTransaction = true; + } else { + canExecuteInTransaction = detectCanExecuteInTransaction(simplifiedStatement, keywords); + } + } + + + + + + } + } while (true); + } catch (Exception e) { + IOUtils.close(reader); + String docsPage = "https://flywaydb.org/documentation/knownparserlimitations"; + throw new FlywayException("Unable to parse statement in " + resource.getAbsolutePath() + + " at line " + statementLine + " col " + statementCol + ". See " + docsPage + " for more information: " + e.getMessage(), e); + } + } + + protected boolean shouldAdjustBlockDepth(ParserContext context, Token token) { + return (token.getType() == TokenType.KEYWORD && token.getParensDepth() == 0); + } + + /** + * Whether the current set of tokens should be discarded. + * + * @param token The latest token. + * @param nonCommentPartSeen Whether a non-comment part has already be seen. + * @return {@code true} if it should, {@code false} if not. + */ + protected boolean shouldDiscard(Token token, boolean nonCommentPartSeen) { + TokenType tokenType = token.getType(); + return (tokenType == TokenType.DELIMITER || tokenType == TokenType.BLANK_LINES) && !nonCommentPartSeen; + } + + /** + * Resets the delimiter to its default value before parsing a new statement. + */ + protected void resetDelimiter(ParserContext context) { + context.setDelimiter(getDefaultDelimiter()); + } + + /** + * Adjusts the delimiter if necessary for this statement type. + * + * @param statementType The statement type. + */ + protected void adjustDelimiter(ParserContext context, StatementType statementType) { + } + + /** + * @return The cutoff point in terms of number of tokens after which a statement can no longer be non-transactional. + */ + protected int getTransactionalDetectionCutoff() { + return 10; + } + + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + } + + protected static int getLastKeywordIndex(List tokens) { + return getLastKeywordIndex(tokens, tokens.size()); + } + + protected static int getLastKeywordIndex(List tokens, int endIndex) { + for (int i = endIndex - 1; i >= 0; i--) { + Token token = tokens.get(i); + if (token.getType() == TokenType.KEYWORD) { + return i; + } + } + return -1; + } + + static String keywordToUpperCase(String text) { + if (!containsLowerCase(text)) { + return text; + } + + StringBuilder result = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c >= 'a' && c <= 'z') { + result.append((char) (c - ('a' - 'A'))); + } else { + result.append(c); + } + } + return result.toString(); + } + + private static boolean containsLowerCase(String text) { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c >= 'a' && c <= 'z') { + return true; + } + } + return false; + } + + /** + * Returns the last token at the given parensDepth. Skips comments and blank lines. Will return null if no token found. + */ + protected static Token getPreviousToken(List tokens, int parensDepth) { + for (int i = tokens.size()-1; i >= 0; i--) { + Token previousToken = tokens.get(i); + + // Only consider tokens at the same parenthesis depth + if (previousToken.getParensDepth() != parensDepth) { + continue; + } + // Skip over comments and blank lines + if (previousToken.getType() == TokenType.COMMENT || previousToken.getType() == TokenType.BLANK_LINES) { + continue; + } + + return previousToken; + } + + return null; + } + + /** + * Returns true if the previous token matches the tokenText + */ + protected static boolean lastTokenIs(List tokens, int parensDepth, String tokenText) { + Token previousToken = getPreviousToken(tokens, parensDepth); + if (previousToken == null) { + return false; + } + + return tokenText.equals(previousToken.getText()); + } + + /** + * Check if the previous tokens in the statement at the same depth as the current token match the provided regex + */ + protected static boolean doTokensMatchPattern(List previousTokens, Token current, Pattern regex) { + ArrayList tokenStrings = new ArrayList<>(); + tokenStrings.add(current.getText()); + + for (int i = previousTokens.size()-1; i >= 0; i--) { + Token prevToken = previousTokens.get(i); + if (prevToken.getParensDepth() != current.getParensDepth()) { + break; + } + + if (prevToken.getType() == TokenType.KEYWORD) { + tokenStrings.add(prevToken.getText()); + } + } + + StringBuilder builder = new StringBuilder(); + for (int i = tokenStrings.size()-1; i >= 0; i--) { + builder.append(tokenStrings.get(i)); + if (i != 0) { + builder.append(" "); + } + } + + return regex.matcher(builder.toString()).matches(); + } + + protected ParsedSqlStatement createStatement(PeekingReader reader, Recorder recorder, + int statementPos, int statementLine, int statementCol, + int nonCommentPartPos, int nonCommentPartLine, int nonCommentPartCol, + StatementType statementType, boolean canExecuteInTransaction, + Delimiter delimiter, String sql + + + + ) throws IOException { + return new ParsedSqlStatement(statementPos, statementLine, statementCol, + sql, delimiter, canExecuteInTransaction + + + + ); + } + + protected StatementType detectStatementType(String simplifiedStatement) { + return StatementType.UNKNOWN; + } + + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + return true; + } + + + + + + + + + + + + private Token readToken(PeekingReader reader, PositionTracker tracker, ParserContext context) throws IOException { + int pos = tracker.getPos(); + int line = tracker.getLine(); + int col = tracker.getCol(); + + String peek = reader.peek(peekDepth); + if (peek == null) { + return new Token(TokenType.EOF, pos, line, col, null, null, 0); + } + char c = peek.charAt(0); + if (isAlternativeStringLiteral(peek)) { + return handleAlternativeStringLiteral(reader, context, pos, line, col); + } + if (c == '\'') { + return handleStringLiteral(reader, context, pos, line, col); + } + if (c == '(') { + context.increaseParensDepth(); + reader.swallow(); + return null; + } + if (c == ')') { + context.decreaseParensDepth(); + reader.swallow(); + return null; + } + if (c == identifierQuote || c == alternativeIdentifierQuote) { + reader.swallow(); + String text = reader.readUntilExcludingWithEscape(c, true); + if (reader.peek('.')) { + text = readAdditionalIdentifierParts(reader, c, context.getDelimiter(), context); + } + return new Token(TokenType.IDENTIFIER, pos, line, col, text, text, context.getParensDepth()); + } + if (isCommentDirective(peek)) { + return handleCommentDirective(reader, context, pos, line, col); + } + if (isSingleLineComment(peek, context, col)) { + reader.swallowUntilExcluding('\n', '\r'); + return new Token(TokenType.COMMENT, pos, line, col, null, null, context.getParensDepth()); + } + if (peek.startsWith("/*")) { + reader.swallow(2); + reader.swallowUntilExcluding("*/"); + reader.swallow(2); + return new Token(TokenType.COMMENT, pos, line, col, null, null, context.getParensDepth()); + } + if (isDigit(c)) { + String text = reader.readNumeric(); + return new Token(TokenType.NUMERIC, pos, line, col, text, text, context.getParensDepth()); + } + if (peek.startsWith("B'") || peek.startsWith("E'") || peek.startsWith("X'")) { + reader.swallow(2); + reader.swallowUntilExcludingWithEscape('\'', true, '\\'); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + if (peek.startsWith("U&'")) { + reader.swallow(3); + reader.swallowUntilExcludingWithEscape('\'', true); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + if (isDelimiter(peek, context, col)) { + return handleDelimiter(reader, context, pos, line, col); + } + if (c == '_' || context.isLetter(c)) { + String text = readKeyword(reader, context.getDelimiter(), context); + if (reader.peek('.')) { + text += readAdditionalIdentifierParts(reader, identifierQuote, context.getDelimiter(), context); + } + if (!isKeyword(text)) { + return new Token(TokenType.IDENTIFIER, pos, line, col, text, text, context.getParensDepth()); + } + return handleKeyword(reader, context, pos, line, col, text); + } + if (c == ' ' || c == '\r' || c == '\u00A0' /* Non-linebreaking space */) { + reader.swallow(); + return null; + } + if (Character.isWhitespace(c)) { + String text = reader.readWhitespace(); + if (containsAtLeast(text, '\n', 2)) { + return new Token(TokenType.BLANK_LINES, pos, line, col, null, null, context.getParensDepth()); + } + return null; + } + + String text = "" + (char) reader.read(); + return new Token(TokenType.SYMBOL, pos, line, col, text, text, context.getParensDepth()); + } + + protected String readKeyword(PeekingReader reader, Delimiter delimiter, ParserContext context) throws IOException { + return "" + (char) reader.read() + reader.readKeywordPart(delimiter, context); + } + + protected Token handleDelimiter(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + Delimiter delimiter = context.getDelimiter(); + String text = delimiter.getDelimiter(); + reader.swallow(text.length()); + return new Token(TokenType.DELIMITER, pos, line, col, text, text, context.getParensDepth()); + } + + protected boolean isAlternativeStringLiteral(String peek) { + return alternativeStringLiteralQuote != 0 && peek.charAt(0) == alternativeStringLiteralQuote; + } + + protected boolean isDelimiter(String peek, ParserContext context, int col) { + Delimiter delimiter = context.getDelimiter(); + return peek.startsWith(delimiter.getDelimiter()); + } + + protected boolean isSingleLineComment(String peek, ParserContext context, int col) { + return peek.startsWith("--"); + } + + /** + * Checks whether this is a keyword ({@code true}) or not ({@code false} = identifier, ...). + * + * @param text The token to check. + * @return {@code true} if it is, {@code false} if not. + */ + protected boolean isKeyword(String text) { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')) { + return false; + } + } + if (validKeywords != null) { + return validKeywords.contains(text); + } + return true; + } + + @SuppressWarnings("Duplicates") + private String readAdditionalIdentifierParts(PeekingReader reader, char quote, Delimiter delimiter, ParserContext context) throws IOException { + String result = ""; + reader.swallow(); + result += "."; + if (reader.peek(quote)) { + reader.swallow(); + result += reader.readUntilExcludingWithEscape(quote, true); + } else { + result += reader.readKeywordPart(delimiter, context); + } + if (reader.peek('.')) { + reader.swallow(); + result += "."; + if (reader.peek(quote)) { + reader.swallow(); + result += reader.readUntilExcludingWithEscape(quote, true); + } else { + result += reader.readKeywordPart(delimiter, context); + } + } + return result; + } + + protected boolean isCommentDirective(String peek) { + return false; + } + + protected Token handleCommentDirective(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + return null; + } + + protected Token handleStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(); + reader.swallowUntilExcludingWithEscape('\'', true); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + return null; + } + + protected Token handleKeyword(PeekingReader reader, ParserContext context, int pos, int line, int col, String keyword) throws IOException { + return new Token(TokenType.KEYWORD, pos, line, col, keywordToUpperCase(keyword), keyword, context.getParensDepth()); + } + + private static boolean containsAtLeast(String str, char c, int min) { + if (min > str.length()) { + return false; + } + + int count = 0; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == c) { + count++; + if (count >= min) { + return true; + } + } + } + return false; + } + + protected static boolean keywordIs(String expected, String actual) { + if (expected.length() != actual.length()) { + return false; + } + for (int i = 0; i < expected.length(); i++) { + char ce = expected.charAt(i); + char ca = actual.charAt(i); + + if (ce != ca && ce + ('a' - 'A') != ca) { + return false; + } + } + + return true; + } + + protected static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + public class ParserSqlStatementIterator implements SqlStatementIterator { + private final PeekingReader peekingReader; + private final LoadableResource resource; + private final Recorder recorder; + private final PositionTracker tracker; + private final ParserContext context; + + public ParserSqlStatementIterator(PeekingReader peekingReader, LoadableResource resource, Recorder recorder, PositionTracker tracker, ParserContext context) { + this.peekingReader = peekingReader; + this.resource = resource; + this.recorder = recorder; + this.tracker = tracker; + this.context = context; + nextStatement = getNextStatement(resource, peekingReader, recorder, tracker, context); + } + + @Override + public void close() { + IOUtils.close(peekingReader); + } + + private SqlStatement nextStatement; + + @Override + public boolean hasNext() { + return nextStatement != null; + } + + @Override + public SqlStatement next() { + if (nextStatement == null) { + throw new NoSuchElementException("No more statements in " + resource.getFilename()); + } + + SqlStatement result = nextStatement; + nextStatement = getNextStatement(resource, peekingReader, recorder, tracker, context); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java new file mode 100644 index 00000000..6412c2a6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.sqlscript.Delimiter; + +import java.security.InvalidParameterException; + +public class ParserContext { + private int parensDepth = 0; + private int blockDepth = 0; + private Delimiter delimiter; + private StatementType statementType; + + public ParserContext(Delimiter delimiter) { + this.delimiter = delimiter; + } + + public void increaseParensDepth() { + parensDepth++; + } + + public void decreaseParensDepth() { + parensDepth--; + } + + public int getParensDepth() { + return parensDepth; + } + + public void increaseBlockDepth() { + blockDepth++; + } + + public void decreaseBlockDepth() { + if (blockDepth == 0) { + throw new FlywayException("Flyway parsing bug: unable to decrease block depth below 0"); + } + blockDepth--; + } + + public int getBlockDepth() { + return blockDepth; + } + + public Delimiter getDelimiter() { + return delimiter; + } + + public void setDelimiter(Delimiter delimiter) { + this.delimiter = delimiter; + } + + public StatementType getStatementType() { + return statementType; + } + + public void setStatementType(StatementType statementType) { + if (statementType == null) { + throw new InvalidParameterException("statementType must be non-null"); + } + + this.statementType = statementType; + } + + public boolean isLetter(char c) { + if (Character.isLetter(c)) { + return true; + } + // Some statement types admit other characters as letters + if (getStatementType() != StatementType.UNKNOWN) { + return statementType.treatAsIfLetter(c); + } + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParsingContext.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParsingContext.java new file mode 100644 index 00000000..638f9f64 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParsingContext.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; + +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class ParsingContext { + private static final Log LOG = LogFactory.getLog(ParsingContext.class); + + private static final String DEFAULT_SCHEMA_PLACEHOLDER = "flyway:defaultSchema"; + private static final String USER_PLACEHOLDER = "flyway:user"; + private static final String DATABASE_PLACEHOLDER = "flyway:database"; + private static final String TIMESTAMP_PLACEHOLDER = "flyway:timestamp"; + + private Map placeholders = new HashMap<>(); + + public Map getPlaceholders() { + return placeholders; + } + + public void populate(Database database, Configuration configuration) { + String defaultSchemaName = configuration.getDefaultSchema(); + String[] schemaNames = configuration.getSchemas(); + + Schema currentSchema = getCurrentSchema(database); + String catalog = getCatalog(database); + String currentUser = getCurrentUser(database); + + // cf. Flyway.prepareSchemas() + if (defaultSchemaName == null) { + if (schemaNames.length > 0) { + defaultSchemaName = schemaNames[0]; + } else { + defaultSchemaName = currentSchema.getName(); + } + } + + if (defaultSchemaName != null) { + placeholders.put(DEFAULT_SCHEMA_PLACEHOLDER, defaultSchemaName); + } + + if (catalog != null) { + placeholders.put(DATABASE_PLACEHOLDER, catalog); + } + + placeholders.put(USER_PLACEHOLDER, currentUser); + placeholders.put(TIMESTAMP_PLACEHOLDER, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + } + + private String getCatalog(Database database) { + try { + return database.getMainConnection().getJdbcConnection().getCatalog(); + } catch (SQLException e) { + LOG.debug("Could not get database name for " + DATABASE_PLACEHOLDER + " placeholder."); + return null; + } + } + + private Schema getCurrentSchema(Database database) { + try { + return database.getMainConnection().getCurrentSchema(); + } catch (FlywayException e) { + LOG.debug("Could not get schema for " + DEFAULT_SCHEMA_PLACEHOLDER + " placeholder."); + return null; + } + } + + private String getCurrentUser(Database database) { + try { + return database.getCurrentUser(); + } catch (FlywayException e) { + LOG.debug("Could not get user for " + USER_PLACEHOLDER + " placeholder."); + return null; + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java new file mode 100644 index 00000000..674317ac --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java @@ -0,0 +1,487 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import org.flywaydb.core.internal.sqlscript.Delimiter; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; + +public class PeekingReader extends FilterReader { + private int[] peekBuffer = new int[256]; + private int peekMax = 0; + private int peekBufferOffset = 0; + + PeekingReader(Reader in) { + super(in); + } + + @Override + public int read() throws IOException { + peekBufferOffset++; + return super.read(); + } + + /** + * Swallows the next character. + */ + public void swallow() throws IOException { + //noinspection ResultOfMethodCallIgnored + read(); + } + + /** + * Swallows the next n characters. + */ + public void swallow(int n) throws IOException { + for (int i = 0; i < n; i++) { + //noinspection ResultOfMethodCallIgnored + read(); + } + } + + private int peek() throws IOException { + if (peekBufferOffset >= peekMax) { + refillPeekBuffer(); + } + + return peekBuffer[peekBufferOffset]; + } + + private void refillPeekBuffer() throws IOException { + mark(peekBuffer.length); + peekMax = peekBuffer.length; + peekBufferOffset = 0; + for (int i = 0; i < peekBuffer.length; i++) { + int read = super.read(); + peekBuffer[i] = read; + if (read == '\n') { + peekMax = i; + break; + } + } + reset(); + } + + /** + * Peek ahead in the stream to see if the next character matches this one. + * + * @param c The character to match. + * @return {@code true} if it does, {@code false} if not. + */ + public boolean peek(char c) throws IOException { + int r = peek(); + return r != -1 && c == (char) r; + } + + /** + * Peek ahead in the stream to see if the next character matches either of these. + * + * @param c1 The first character to match. + * @param c2 The second character to match. + * @return {@code true} if it does, {@code false} if not. + */ + public boolean peek(char c1, char c2) throws IOException { + int r = peek(); + return r != -1 && (c1 == (char) r || c2 == (char) r); + } + + /** + * Peek ahead in the stream to see if the next character is numeric. + * + * @return {@code true} if it is, {@code false} if not. + */ + public boolean peekNumeric() throws IOException { + int r = peek(); + return isNumeric(r); + } + + private boolean isNumeric(int r) { + return r != -1 && (char) r >= '0' && (char) r <= '9'; + } + + /** + * Peek ahead in the stream to see if the next character is whitespace. + * + * @return {@code true} if it is, {@code false} if not. + */ + public boolean peekWhitespace() throws IOException { + int r = peek(); + return isWhitespace(r); + } + + private boolean isWhitespace(int r) { + return r != -1 && Character.isWhitespace((char) r); + } + + /** + * Peek ahead in the stream to see if the next character could be a character part of a keyword or identifier. + * + * @return {@code true} if it is, {@code false} if not. + */ + public boolean peekKeywordPart(ParserContext context) throws IOException { + int r = peek(); + return isKeywordPart(r, context); + } + + private boolean isKeywordPart(int r, ParserContext context) { + return r != -1 && ((char) r == '_' || (char) r == '$' || Character.isLetterOrDigit((char) r) || context.isLetter((char)r)); + } + + /** + * Peek ahead in the stream to see if the next characters match this string exactly. + * + * @param str The string to match. + * @return {@code true} if they do, {@code false} if not. + */ + public boolean peek(String str) throws IOException { + return str.equals(peek(str.length())); + } + + /** + * Peek ahead in the stream to look at this number of characters ahead in the reader. + * + * @param numChars The number of characters. + * @return The characters. + */ + public String peek(int numChars) throws IOException { + // If we need to peek beyond the physical size of the peek buffer - eg. we have encountered a very + // long string literal - then expand the buffer to be big enough to contain it. + if (numChars >= peekBuffer.length) { + resizePeekBuffer(numChars); + } + + if (peekBufferOffset + numChars >= peekMax) { + refillPeekBuffer(); + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < numChars; i++) { + int r = peekBuffer[peekBufferOffset + i]; + if (r == -1) { + break; + } else if (peekBufferOffset + i > peekMax) { + break; + } + result.append((char) r); + } + if (result.length() == 0) { + return null; + } + return result.toString(); + } + + /** + * Return the next non-whitespace character + * @return The character + */ + public char peekNextNonWhitespace() throws IOException { + int i = 1; + String c = peek(i++); + while (c.trim().isEmpty()) { + c = peek(i++); + } + + return c.charAt(c.length()-1); + } + + private void resizePeekBuffer(int newSize) { + peekBuffer = Arrays.copyOf(peekBuffer, newSize + peekBufferOffset); + } + + /** + * Swallows all characters in this stream until any of these delimiting characters has been encountered. + * + * @param delimiter1 The first delimiting character. + * @param delimiter2 The second delimiting character. + */ + public void swallowUntilExcluding(char delimiter1, char delimiter2) throws IOException { + do { + if (peek(delimiter1, delimiter2)) { + break; + } + int r = read(); + if (r == -1) { + break; + } + } while (true); + } + + /** + * Reads all characters in this stream until any of these delimiting characters has been encountered. + * + * @param delimiter1 The first delimiting character. + * @param delimiter2 The second delimiting character. + * @return The string read, without the delimiting characters. + */ + public String readUntilExcluding(char delimiter1, char delimiter2) throws IOException { + StringBuilder result = new StringBuilder(); + do { + if (peek(delimiter1, delimiter2)) { + break; + } + int r = read(); + if (r == -1) { + break; + } else { + result.append((char) r); + } + } while (true); + return result.toString(); + } + + /** + * Swallows all characters in this stream until this delimiting character has been encountered, taking into account + * this escape character for the delimiting character. + * + * @param delimiter The delimiting character. + * @param selfEscape Whether the delimiter can escape itself by being present twice. + */ + public void swallowUntilExcludingWithEscape(char delimiter, boolean selfEscape) throws IOException { + swallowUntilExcludingWithEscape(delimiter, selfEscape, (char) 0); + } + + /** + * Swallows all characters in this stream until this delimiting character has been encountered, taking into account + * this escape character for the delimiting character. + * + * @param delimiter The delimiting character. + * @param selfEscape Whether the delimiter can escape itself by being present twice. + * @param escape A separate escape character. + */ + public void swallowUntilExcludingWithEscape(char delimiter, boolean selfEscape, char escape) throws IOException { + do { + int r = read(); + if (r == -1) { + break; + } + char c = (char) r; + if (escape != 0 && c == escape) { + swallow(); + continue; + } + if (c == delimiter) { + if (selfEscape && peek(delimiter)) { + swallow(); + continue; + } + break; + } + } while (true); + } + + /** + * Reads all characters in this stream until this delimiting character has been encountered, taking into account + * this escape character for the delimiting character. + * + * @param delimiter The delimiting character. + * @param selfEscape Whether the delimiter can escape itself by being present twice. + * @return The string read, without the delimiting character. + */ + public String readUntilExcludingWithEscape(char delimiter, boolean selfEscape) throws IOException { + return readUntilExcludingWithEscape(delimiter, selfEscape, (char) 0); + } + + /** + * Reads all characters in this stream until this delimiting character has been encountered, taking into account + * this escape character for the delimiting character. + * + * @param delimiter The delimiting character. + * @param selfEscape Whether the delimiter can escape itself by being present twice. + * @param escape A separate escape character. + * @return The string read, without the delimiting character. + */ + public String readUntilExcludingWithEscape(char delimiter, boolean selfEscape, char escape) throws IOException { + StringBuilder result = new StringBuilder(); + do { + int r = read(); + if (r == -1) { + break; + } + char c = (char) r; + if (escape != 0 && c == escape) { + int r2 = read(); + if (r2 == -1) { + result.append(escape); + break; + } + char c2 = (char) r2; + result.append(c2); + continue; + } + if (c == delimiter) { + if (selfEscape && peek(delimiter)) { + result.append(delimiter); + continue; + } + break; + } + result.append(c); + } while (true); + return result.toString(); + } + + /** + * Swallows all characters in this stream until this delimiting string has been encountered. + * + * @param str The delimiting string. + */ + public void swallowUntilExcluding(String str) throws IOException { + do { + if (peek(str)) { + break; + } + int r = read(); + if (r == -1) { + break; + } + } while (true); + } + + /** + * Reads all characters in this stream until this delimiting string has been encountered. + * + * @param str The delimiting string. + * @return The string read, without the delimiting string. + */ + public String readUntilExcluding(String str) throws IOException { + StringBuilder result = new StringBuilder(); + do { + if (peek(str)) { + break; + } + int r = read(); + if (r == -1) { + break; + } else { + result.append((char) r); + } + } while (true); + return result.toString(); + } + + /** + * Reads all characters in this stream until any of this delimiting character has been encountered. + * + * @param delimiter The delimiting character. + * @return The string read, including the delimiting characters. + */ + public String readUntilIncluding(char delimiter) throws IOException { + StringBuilder result = new StringBuilder(); + do { + int r = read(); + if (r == -1) { + break; + } + char c = (char) r; + result.append(c); + if (c == delimiter) { + break; + } + } while (true); + return result.toString(); + } + + /** + * Reads all characters in this stream until the delimiting sequence is encountered. + * + * @param delimiterSequence The delimiting sequence. + * @return The string read, including the delimiting characters. + */ + public String readUntilIncluding(String delimiterSequence) throws IOException { + StringBuilder result = new StringBuilder(); + + do { + int r = read(); + if (r == -1) { + break; + } + char c = (char) r; + + result.append(c); + if (result.toString().endsWith(delimiterSequence)) { + break; + } + } while (true); + return result.toString(); + } + + /** + * Reads all characters in this stream as long as they can be part of a keyword. + * + * @param delimiter The current delimiter. + * @return The string read. + */ + public String readKeywordPart(Delimiter delimiter, ParserContext context) throws IOException { + StringBuilder result = new StringBuilder(); + do { + if ((delimiter == null || !peek(delimiter.getDelimiter())) && peekKeywordPart(context)) { + result.append((char) read()); + } else { + break; + } + } while (true); + return result.toString(); + } + + /** + * Swallows all characters in this stream as long as they can be part of a numeric constant. + */ + public void swallowNumeric() throws IOException { + do { + if (!peekNumeric()) { + return; + } + swallow(); + } while (true); + } + + /** + * Reads all characters in this stream as long as they can be part of a numeric constant. + * + * @return The string read. + */ + public String readNumeric() throws IOException { + StringBuilder result = new StringBuilder(); + do { + if (peekNumeric()) { + result.append((char) read()); + } else { + break; + } + } while (true); + return result.toString(); + } + + /** + * Reads all characters in this stream as long as they are whitespace. + * + * @return The string read. + */ + public String readWhitespace() throws IOException { + StringBuilder result = new StringBuilder(); + do { + if (peekWhitespace()) { + result.append((char) read()); + } else { + break; + } + } while (true); + return result.toString(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.java new file mode 100644 index 00000000..ea9abedb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.java @@ -0,0 +1,196 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +public class PlaceholderReplacingReader extends FilterReader { + private final String prefix; + private final String suffix; + private final Map placeholders; + + /** + * The number of chars by which to increase the read-ahead limit to factor in the difference in length between + * placeholders (with prefix and suffix) and their replacements. + */ + private final int readAheadLimitAdjustment; + + private final StringBuilder buffer = new StringBuilder(); + private String markBuffer; + + private String replacement; + private int replacementPos; + + private String markReplacement; + private int markReplacementPos; + + public PlaceholderReplacingReader(String prefix, String suffix, Map placeholders, Reader in) { + super(in); + this.prefix = prefix; + this.suffix = suffix; + this.placeholders = placeholders; + + int prefixSuffixLength = prefix.length() + suffix.length(); + int maxPlaceholderLength = prefixSuffixLength; + int minReplacementLength = Integer.MAX_VALUE; + for (Map.Entry entry : placeholders.entrySet()) { + maxPlaceholderLength = Math.max(maxPlaceholderLength, prefixSuffixLength + entry.getKey().length()); + int valueLength = (entry.getValue() != null) ? entry.getValue().length() : 0; + minReplacementLength = Math.min(minReplacementLength, valueLength); + } + readAheadLimitAdjustment = Math.max(maxPlaceholderLength - minReplacementLength, 0); + } + + public static PlaceholderReplacingReader create(Configuration configuration, ParsingContext parsingContext, Reader reader) { + Map placeholders = new HashMap<>(); + Map configurationPlaceholders = configuration.getPlaceholders(); + Map parsingContextPlaceholders = parsingContext.getPlaceholders(); + + placeholders.putAll(configurationPlaceholders); + placeholders.putAll(parsingContextPlaceholders); + + return new PlaceholderReplacingReader( + configuration.getPlaceholderPrefix(), + configuration.getPlaceholderSuffix(), + placeholders, + reader); + } + + @Override + public int read() throws IOException { + if (replacement == null) { + if (buffer.length() > 0) { + char c = buffer.charAt(0); + buffer.deleteCharAt(0); + return c; + } + + int r; + do { + r = super.read(); + if (r == -1) { + break; + } + + buffer.append((char) r); + } while (buffer.length() < prefix.length() && endsWith(buffer, prefix.substring(0, buffer.length()))); + if (!endsWith(buffer, prefix)) { + if (buffer.length() > 0) { + char c = buffer.charAt(0); + buffer.deleteCharAt(0); + return c; + } + return -1; + } + buffer.delete(0, buffer.length()); + + StringBuilder placeholderBuilder = new StringBuilder(); + do { + int r1 = in.read(); + if (r1 == -1) { + break; + } else { + placeholderBuilder.append((char) r1); + } + } while (!endsWith(placeholderBuilder, suffix)); + for (int i = 0; i < suffix.length(); i++) { + placeholderBuilder.deleteCharAt(placeholderBuilder.length() - 1); + } + + + String placeholder = placeholderBuilder.toString(); + if (!placeholders.containsKey(placeholder)) { + String canonicalPlaceholder = prefix + placeholder + suffix; + + if (placeholder.contains("flyway:")) { + throw new FlywayException("Failed to populate value for default placeholder: " + + canonicalPlaceholder); + } + + throw new FlywayException("No value provided for placeholder: " + + canonicalPlaceholder + + ". Check your configuration!"); + } + + replacement = placeholders.get(placeholder); + + // Empty placeholder value -> move to the next character + if (replacement == null || replacement.length() == 0) { + replacement = null; + return read(); + } + } + + int result = replacement.charAt(replacementPos); + replacementPos++; + if (replacementPos >= replacement.length()) { + replacement = null; + replacementPos = 0; + } + return result; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + int count = 0; + for (int i = 0; i < len; i++) { + int r = read(); + if (r == -1) { + return count == 0 ? -1 : count; + } + cbuf[off + i] = (char) r; + count++; + } + return count; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + markBuffer = buffer.toString(); + markReplacement = replacement; + markReplacementPos = replacementPos; + super.mark(readAheadLimit + readAheadLimitAdjustment); + } + + @Override + public void reset() throws IOException { + super.reset(); + buffer.delete(0, buffer.length()); + buffer.append(markBuffer); + replacement = markReplacement; + replacementPos = markReplacementPos; + } + + private boolean endsWith(StringBuilder result, String str) { + if (result.length() < str.length()) { + return false; + } + + for (int i = 0; i < str.length(); i++) { + if (result.charAt(result.length() - str.length() + i) != str.charAt(i)) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTracker.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTracker.java new file mode 100644 index 00000000..58733073 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTracker.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +public class PositionTracker { + private int pos = 0; + private int line = 1; + private int col = 1; + + private int markPos = 0; + private int markLine = 1; + private int markCol = 1; + + public int getPos() { + return pos; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public void nextPos() { + pos++; + } + + public void nextCol() { + col++; + } + + public void linefeed() { + line++; + col = 1; + } + + public void carriageReturn() { + col = 1; + } + + public void mark() { + markPos = pos; + markLine = line; + markCol = col; + } + + public void reset() { + pos = markPos; + line = markLine; + col = markCol; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTrackingReader.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTrackingReader.java new file mode 100644 index 00000000..66fef677 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTrackingReader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +public class PositionTrackingReader extends FilterReader { + private final PositionTracker tracker; + private boolean paused; + + PositionTrackingReader(PositionTracker tracker, Reader in) { + super(in); + this.tracker = tracker; + } + + @Override + public int read() throws IOException { + int read = super.read(); + if (read != -1 && !paused) { + tracker.nextPos(); + char c = (char) read; + if (c == '\n') { + tracker.linefeed(); + } else if (c == '\r') { + tracker.carriageReturn(); + } else { + tracker.nextCol(); + } + } + return read; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + paused = true; + super.mark(readAheadLimit); + } + + @Override + public void reset() throws IOException { + super.reset(); + paused = false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Recorder.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Recorder.java new file mode 100644 index 00000000..23e4a69e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Recorder.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +public class Recorder { + private StringBuilder recorder; + private boolean recorderPaused = false; + private int recorderConfirmedPos = 0; + + public void record(char c) { + if (isRunninng()) { + recorder.append(c); + } + } + + public int length() { + return recorder.length(); + } + + public void truncate(int length) { + if (isRunninng()) { + recorder.delete(length, recorder.length()); + } + } + + private boolean isRunninng() { + return recorder != null && !recorderPaused; + } + + public void start() { + recorder = new StringBuilder(); + recorderConfirmedPos = 0; + recorderPaused = false; + } + + public void pause() { + recorderPaused = true; + } + + public void unpause() { + recorderPaused = false; + } + + public void record(String str) { + recorder.append(str); + confirm(); + } + + public void confirm() { + recorderConfirmedPos = recorder.length(); + } + + public String stop() { + // Drop unconfirmed parts of recording + recorder.delete(recorderConfirmedPos, recorder.length()); + + String result = recorder.toString(); + recorder = null; + return result; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/RecordingReader.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/RecordingReader.java new file mode 100644 index 00000000..4b91b0fb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/RecordingReader.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +public class RecordingReader extends FilterReader { + private boolean paused; + private Recorder recorder; + + RecordingReader(Recorder recorder, Reader in) { + super(in); + this.recorder = recorder; + } + + @Override + public int read() throws IOException { + int read = super.read(); + if (read != -1 && !paused) { + recorder.record((char) read); + } + return read; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + paused = true; + super.mark(readAheadLimit); + } + + @Override + public void reset() throws IOException { + super.reset(); + paused = false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Statement.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Statement.java new file mode 100644 index 00000000..c174f3e5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Statement.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +import java.util.List; + +class Statement { + private final int pos; + private final int line; + private final int col; + private final StatementType statementType; + private final String sql; + private final List tokens; + + Statement(int pos, int line, int col, StatementType statementType, String sql, List tokens) { + this.pos = pos; + this.line = line; + this.col = col; + this.statementType = statementType; + this.sql = sql; + this.tokens = tokens; + } + + public int getPos() { + return pos; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public StatementType getStatementType() { + return statementType; + } + + public String getSql() { + return sql; + } + + public List getTokens() { + return tokens; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/StatementType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/StatementType.java new file mode 100644 index 00000000..68f92e0c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/StatementType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +public class StatementType { + public static final StatementType GENERIC = new StatementType(); + public static final StatementType UNKNOWN = new StatementType(); + + /** + * Whether the character should be treated as if it is a letter; this allows statement types to handle + * characters that appear in specific contexts + * @param c + * @return + */ + public boolean treatAsIfLetter(char c) { + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Token.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Token.java new file mode 100644 index 00000000..67967b06 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Token.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +public class Token { + private final TokenType type; + private final int pos; + private final int line; + private final int col; + private final String text; + private final String rawText; + private final int parensDepth; + + public Token(TokenType type, int pos, int line, int col, String text, String rawText, int parensDepth) { + this.type = type; + this.pos = pos; + this.line = line; + this.col = col; + this.text = text; + this.rawText = rawText; + this.parensDepth = parensDepth; + } + + public TokenType getType() { + return type; + } + + public int getPos() { + return pos; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public String getText() { + return text; + } + + public String getRawText() { + return rawText; + } + + public int getParensDepth() { + return parensDepth; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/TokenType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/TokenType.java new file mode 100644 index 00000000..c6df0628 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/TokenType.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.parser; + +public enum TokenType { + KEYWORD, + + /** + * An identifier, referring to a schema object like a table or column. + */ + IDENTIFIER, + + NUMERIC, + + /** + * A string literal. + */ + STRING, + + /** + * A comment in front of or within a statement. Can be single line (--) or multi-line (/* */). + */ + COMMENT, + + /** + * An actual statement disguised as a multi-line comment. + */ + MULTI_LINE_COMMENT_DIRECTIVE, + + /** + * ( + */ + PARENS_OPEN, + + /** + * ) + */ + PARENS_CLOSE, + + DELIMITER, + + /** + * The new delimiter that will be used from now on. + */ + NEW_DELIMITER, + + /** + * A symbol such as ! or #. + */ + SYMBOL, + + BLANK_LINES, + EOF, + COPY_DATA +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/package-info.java new file mode 100644 index 00000000..a54f9f6c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.parser; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java new file mode 100644 index 00000000..fb8441d7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.util.BomFilter; +import org.flywaydb.core.internal.util.IOUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.zip.CRC32; + +public class ChecksumCalculator { + private ChecksumCalculator() { + // Private constructor to prevent instantiation + } + + /** + * Calculates the checksum of these resources. The checksum is encoding and line-ending independent. + * + * @return The crc-32 checksum of the bytes. + */ + public static int calculate(LoadableResource... loadableResources) { + int checksum; + + + + + checksum = calculateChecksumForResource(loadableResources[0]); + + + + + + + + + + + + + return checksum; + } + + private static int calculateChecksumForResource(LoadableResource resource) { + final CRC32 crc32 = new CRC32(); + + BufferedReader bufferedReader = null; + try { + bufferedReader = new BufferedReader(resource.read(), 4096); + + String line = bufferedReader.readLine(); + + if (line != null) { + line = BomFilter.FilterBomFromString(line); + + do { + //noinspection Since15 + crc32.update(StringUtils.trimLineBreak(line).getBytes(StandardCharsets.UTF_8)); + } while ((line = bufferedReader.readLine()) != null); + } + } catch (IOException e) { + throw new FlywayException("Unable to calculate checksum of " + resource.getFilename() + "\r\n" + e.getMessage(), e); + } finally { + IOUtils.close(bufferedReader); + } + + return (int) crc32.getValue(); + } + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/CompositeMigrationResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/CompositeMigrationResolver.java new file mode 100644 index 00000000..87390516 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/CompositeMigrationResolver.java @@ -0,0 +1,166 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.clazz.ClassProvider; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.core.internal.resolver.java.FixedJavaMigrationResolver; +import org.flywaydb.core.internal.resolver.java.ScanningJavaMigrationResolver; +import org.flywaydb.core.internal.resolver.sql.SqlMigrationResolver; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Facility for retrieving and sorting the available migrations from the classpath through the various migration + * resolvers. + */ +public class CompositeMigrationResolver implements MigrationResolver { + /** + * The migration resolvers to use internally. + */ + private Collection migrationResolvers = new ArrayList<>(); + + /** + * The available migrations, sorted by version, newest first. An empty list is returned when no migrations can be + * found. + */ + private List availableMigrations; + + /** + * Creates a new CompositeMigrationResolver. + * + * @param resourceProvider The resource provider. + * @param classProvider The class provider. + * @param configuration The Flyway configuration. + * @param sqlScriptFactory The SQL statement builder factory. + * @param customMigrationResolvers Custom Migration Resolvers. + * @param parsingContext The parsing context + */ + public CompositeMigrationResolver(ResourceProvider resourceProvider, + ClassProvider classProvider, + Configuration configuration, + SqlScriptExecutorFactory sqlScriptExecutorFactory, + SqlScriptFactory sqlScriptFactory, + ParsingContext parsingContext, + MigrationResolver... customMigrationResolvers + ) { + if (!configuration.isSkipDefaultResolvers()) { + migrationResolvers.add(new SqlMigrationResolver(resourceProvider, sqlScriptExecutorFactory, sqlScriptFactory, + configuration, parsingContext)); + migrationResolvers.add(new ScanningJavaMigrationResolver(classProvider, configuration)); + } + migrationResolvers.add(new FixedJavaMigrationResolver(configuration.getJavaMigrations())); + + migrationResolvers.addAll(Arrays.asList(customMigrationResolvers)); + } + + /** + * Finds all available migrations using all migration resolvers (sql, java, ...). + * + * @return The available migrations, sorted by version, oldest first. An empty list is returned when no migrations + * can be found. + * @throws FlywayException when the available migrations have overlapping versions. + */ + public List resolveMigrations(Context context) { + if (availableMigrations == null) { + availableMigrations = doFindAvailableMigrations(context); + } + + return availableMigrations; + } + + /** + * Finds all available migrations using all migration resolvers (sql, java, ...). + * + * @return The available migrations, sorted by version, oldest first. An empty list is returned when no migrations + * can be found. + * @throws FlywayException when the available migrations have overlapping versions. + */ + private List doFindAvailableMigrations(Context context) throws FlywayException { + List migrations = new ArrayList<>(collectMigrations(migrationResolvers, context)); + Collections.sort(migrations, new ResolvedMigrationComparator()); + + checkForIncompatibilities(migrations); + + return migrations; + } + + /** + * Collects all the migrations for all migration resolvers. + * + * @param migrationResolvers The migration resolvers to check. + * @return All migrations. + */ + /* private -> for testing */ + static Collection collectMigrations(Collection migrationResolvers, Context context) { + Set migrations = new HashSet<>(); + for (MigrationResolver migrationResolver : migrationResolvers) { + migrations.addAll(migrationResolver.resolveMigrations(context)); + } + return migrations; + } + + /** + * Checks for incompatible migrations. + * + * @param migrations The migrations to check. + * @throws FlywayException when two different migration with the same version number are found. + */ + /* private -> for testing */ + static void checkForIncompatibilities(List migrations) { + ResolvedMigrationComparator resolvedMigrationComparator = new ResolvedMigrationComparator(); + // check for more than one migration with same version + for (int i = 0; i < migrations.size() - 1; i++) { + ResolvedMigration current = migrations.get(i); + ResolvedMigration next = migrations.get(i + 1); + if (resolvedMigrationComparator.compare(current, next) == 0) { + if (current.getVersion() != null) { + throw new FlywayException(String.format("Found more than one migration with version %s\nOffenders:\n-> %s (%s)\n-> %s (%s)", + current.getVersion(), + current.getPhysicalLocation(), + current.getType(), + next.getPhysicalLocation(), + next.getType()), + ErrorCode.DUPLICATE_VERSIONED_MIGRATION); + } + throw new FlywayException(String.format("Found more than one repeatable migration with description %s\nOffenders:\n-> %s (%s)\n-> %s (%s)", + current.getDescription(), + current.getPhysicalLocation(), + current.getType(), + next.getPhysicalLocation(), + next.getType()), + ErrorCode.DUPLICATE_REPEATABLE_MIGRATION); + } + } + } + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/MigrationInfoHelper.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/MigrationInfoHelper.java new file mode 100644 index 00000000..cd65573f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/MigrationInfoHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.internal.util.Pair; +import org.flywaydb.core.internal.util.StringUtils; + +/** + * Parsing support for migrations that use the standard Flyway version + description embedding in their name. These + * migrations have names like 1_2__Description . + */ +public class MigrationInfoHelper { + /** + * Prevents instantiation. + */ + private MigrationInfoHelper() { + //Do nothing. + } + + /** + * Extracts the schema version and the description from a migration name formatted as 1_2__Description. + * + * @param migrationName The migration name to parse. Should not contain any folders or packages. + * @param prefix The migration prefix. + * @param separator The migration separator. + * @param suffixes The migration suffixes. + * @param repeatable Whether this is a repeatable migration. + * @return The extracted schema version. + * @throws FlywayException if the migration name does not follow the standard conventions. + */ + public static Pair extractVersionAndDescription(String migrationName, + String prefix, String separator, + String[] suffixes, boolean repeatable) { + // Only handles Java migrations now + String cleanMigrationName = cleanMigrationName(migrationName, prefix, suffixes); + + int separatorPos = cleanMigrationName.indexOf(separator); + + String version; + String description; + if (separatorPos < 0) { + version = cleanMigrationName; + description = ""; + } else { + version = cleanMigrationName.substring(0, separatorPos); + description = cleanMigrationName.substring(separatorPos + separator.length()).replace("_", " "); + } + + if (StringUtils.hasText(version)) { + if (repeatable) { + throw new FlywayException("Wrong repeatable migration name format: " + migrationName + + " (It cannot contain a version and should look like this: " + + prefix + separator + description + suffixes[0] + ")"); + } + try { + return Pair.of(MigrationVersion.fromVersion(version), description); + } catch (Exception e) { + throw new FlywayException("Wrong versioned migration name format: " + migrationName + + " (could not recognise version number " + version + ")", e); + } + } + + if (!repeatable) { + throw new FlywayException("Wrong versioned migration name format: " + migrationName + + " (It must contain a version and should look like this: " + + prefix + "1.2" + separator + description + suffixes[0] + ")"); + } + return Pair.of(null, description); + } + + private static String cleanMigrationName(String migrationName, String prefix, String[] suffixes) { + for (String suffix : suffixes) { + if (migrationName.endsWith(suffix)) { + return migrationName.substring( + StringUtils.hasLength(prefix) ? prefix.length() : 0, + migrationName.length() - suffix.length()); + } + } + return migrationName; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationComparator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationComparator.java new file mode 100644 index 00000000..c851819d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationComparator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver; + +import org.flywaydb.core.api.resolver.ResolvedMigration; + +import java.util.Comparator; + +/** +* Comparator for ResolvedMigration. +*/ +public class ResolvedMigrationComparator implements Comparator { + @Override + public int compare(ResolvedMigration o1, ResolvedMigration o2) { + if ((o1.getVersion() != null) && o2.getVersion() != null) { + int v = o1.getVersion().compareTo(o2.getVersion()); + + + + + + + + + + + + + + return v; + } + if (o1.getVersion() != null) { + return -1; + } + if (o2.getVersion() != null) { + return 1; + } + return o1.getDescription().compareTo(o2.getDescription()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationImpl.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationImpl.java new file mode 100644 index 00000000..ba57ed11 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.executor.MigrationExecutor; +import org.flywaydb.core.api.resolver.ResolvedMigration; + +import java.util.Objects; + +/** + * A migration available on the classpath. + */ +public class ResolvedMigrationImpl implements ResolvedMigration { + /** + * The target version of this migration. + */ + private final MigrationVersion version; + + /** + * The description of the migration. + */ + private final String description; + + /** + * The name of the script to execute for this migration, relative to its classpath location. + */ + private final String script; + + /** + * The equivalent checksum of the migration. For versioned migrations, this is the same as the checksum. + * For repeatable migrations, it is the checksum calculated prior to placeholder replacement. + */ + private final Integer equivalentChecksum; + + /** + * The checksum of the migration. + */ + private final Integer checksum; + + /** + * The type of migration (INIT, SQL, ...) + */ + private final MigrationType type; + + /** + * The physical location of the migration on disk. + */ + private final String physicalLocation; + + /** + * The executor to run this migration. + */ + private final MigrationExecutor executor; + + /** + * Creates a new resolved migration. + * + * @param version The target version of this migration. + * @param description The description of the migration. + * @param script The name of the script to execute for this migration, relative to its classpath location. + * @param checksum The checksum of the migration. + * @param equivalentChecksum The equivalent checksum of the migration. + * @param type The type of migration (SQL, ...) + * @param physicalLocation The physical location of the migration on disk. + * @param executor The executor to run this migration. + */ + public ResolvedMigrationImpl(MigrationVersion version, String description, String script, + Integer checksum, Integer equivalentChecksum, + MigrationType type, String physicalLocation, MigrationExecutor executor) { + this.version = version; + this.description = description; + this.script = script; + this.checksum = checksum; + this.equivalentChecksum = equivalentChecksum; + this.type = type; + this.physicalLocation = physicalLocation; + this.executor = executor; + } + + @Override + public MigrationVersion getVersion() { + return version; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getScript() { + return script; + } + + @Override + public Integer getChecksum() { + return checksum == null ? + equivalentChecksum : + checksum; + } + + @Override + public MigrationType getType() { + return type; + } + + @Override + public String getPhysicalLocation() { + return physicalLocation; + } + + @Override + public MigrationExecutor getExecutor() { + return executor; + } + + public int compareTo(ResolvedMigrationImpl o) { + return version.compareTo(o.version); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ResolvedMigrationImpl migration = (ResolvedMigrationImpl) o; + + if (checksum != null ? !checksum.equals(migration.checksum) : migration.checksum != null) return false; + if (equivalentChecksum != null ? !equivalentChecksum.equals(migration.equivalentChecksum) : migration.equivalentChecksum != null) return false; + if (description != null ? !description.equals(migration.description) : migration.description != null) + return false; + if (script != null ? !script.equals(migration.script) : migration.script != null) return false; + if (type != migration.type) return false; + return Objects.equals(version, migration.version); + } + + @Override + public int hashCode() { + int result = (version != null ? version.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (script != null ? script.hashCode() : 0); + result = 31 * result + (checksum != null ? checksum.hashCode() : 0); + result = 31 * result + (equivalentChecksum != null ? equivalentChecksum.hashCode() : 0); + result = 31 * result + type.hashCode(); + return result; + } + + @Override + public String toString() { + return "ResolvedMigrationImpl{" + + "version=" + version + + ", description='" + description + '\'' + + ", script='" + script + '\'' + + ", checksum=" + getChecksum() + + ", type=" + type + + ", physicalLocation='" + physicalLocation + '\'' + + ", executor=" + executor + + '}'; + } + + /** + * Validates this resolved migration. + */ + public void validate() { + // Do nothing by default. + } + + @Override + public boolean checksumMatches(Integer checksum) { + return Objects.equals(checksum, this.checksum) || + Objects.equals(checksum, this.equivalentChecksum); + } + + @Override + public boolean checksumMatchesWithoutBeingIdentical(Integer checksum) { + // The checksum in the database matches the one calculated without replacement, but not the one with. + // That is, the script has placeholders and the checksum was originally calculated ignoring their values. + return Objects.equals(checksum, this.equivalentChecksum) + && !Objects.equals(checksum, this.checksum); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/FixedJavaMigrationResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/FixedJavaMigrationResolver.java new file mode 100644 index 00000000..0c804b5e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/FixedJavaMigrationResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.java; + +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.resolver.ResolvedMigrationComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Migration resolver for a fixed set of pre-instantiated Java-based migrations. + */ +public class FixedJavaMigrationResolver implements MigrationResolver { + /** + * The JavaMigration instances to use. + */ + private final JavaMigration[] javaMigrations; + + /** + * Creates a new instance. + * + * @param javaMigrations The JavaMigration instances to use. + */ + public FixedJavaMigrationResolver(JavaMigration... javaMigrations) { + this.javaMigrations = javaMigrations; + } + + @Override + public List resolveMigrations(Context context) { + List migrations = new ArrayList<>(); + + for (JavaMigration javaMigration : javaMigrations) { + migrations.add(new ResolvedJavaMigration(javaMigration)); + } + + Collections.sort(migrations, new ResolvedMigrationComparator()); + return migrations; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/JavaMigrationExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/JavaMigrationExecutor.java new file mode 100644 index 00000000..48220f44 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/JavaMigrationExecutor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.java; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.executor.Context; +import org.flywaydb.core.api.executor.MigrationExecutor; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.internal.database.DatabaseExecutionStrategy; +import org.flywaydb.core.internal.database.DatabaseFactory; +import org.flywaydb.core.internal.database.cockroachdb.CockroachDBRetryingStrategy; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Adapter for executing migrations implementing JavaMigration. + */ +public class JavaMigrationExecutor implements MigrationExecutor { + /** + * The JavaMigration to execute. + */ + private final JavaMigration javaMigration; + + /** + * Creates a new JavaMigrationExecutor. + * + * @param javaMigration The JavaMigration to execute. + */ + JavaMigrationExecutor(JavaMigration javaMigration) { + this.javaMigration = javaMigration; + } + + @Override + public void execute(final Context context) throws SQLException { + DatabaseExecutionStrategy strategy = DatabaseFactory.createExecutionStrategy(context.getConnection()); + strategy.execute(new SqlCallable() { + @Override + public Boolean call() throws SQLException { + executeOnce(context); + return true; + } + }); + } + + private void executeOnce(final Context context) throws SQLException { + try { + javaMigration.migrate(new org.flywaydb.core.api.migration.Context() { + @Override + public Configuration getConfiguration() { + return context.getConfiguration(); + } + + @Override + public Connection getConnection() { + return context.getConnection(); + } + }); + } catch (SQLException e) { + throw e; + } catch (Exception e) { + throw new FlywayException("Migration failed !", e); + } + } + + @Override + public boolean canExecuteInTransaction() { + return javaMigration.canExecuteInTransaction(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ResolvedJavaMigration.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ResolvedJavaMigration.java new file mode 100644 index 00000000..662a8ab9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ResolvedJavaMigration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.java; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl; +import org.flywaydb.core.internal.util.ClassUtils; + +/** + * A resolved Java migration. + */ +public class ResolvedJavaMigration extends ResolvedMigrationImpl { + /** + * Creates a new ResolvedJavaMigration based on this JavaMigration. + * + * @param javaMigration The JavaMigration to use. + */ + public ResolvedJavaMigration(JavaMigration javaMigration) { + super(javaMigration.getVersion(), + javaMigration.getDescription(), + javaMigration.getClass().getName(), + javaMigration.getChecksum(), + null, + + + + MigrationType.JDBC, + ClassUtils.getLocationOnDisk(javaMigration.getClass()), + new JavaMigrationExecutor(javaMigration) + ); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ScanningJavaMigrationResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ScanningJavaMigrationResolver.java new file mode 100644 index 00000000..500291bf --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ScanningJavaMigrationResolver.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.java; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.clazz.ClassProvider; +import org.flywaydb.core.internal.resolver.ResolvedMigrationComparator; +import org.flywaydb.core.internal.util.ClassUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Migration resolver for Java-based migrations. The classes must have a name like R__My_description, V1__Description + * or V1_1_3__Description. + */ +public class ScanningJavaMigrationResolver implements MigrationResolver { + /** + * The Scanner to use. + */ + private final ClassProvider classProvider; + + /** + * The configuration to inject (if necessary) in the migration classes. + */ + private final Configuration configuration; + + /** + * Creates a new instance. + * + * @param classProvider The class provider. + * @param configuration The configuration to inject (if necessary) in the migration classes. + */ + public ScanningJavaMigrationResolver(ClassProvider classProvider, Configuration configuration) { + this.classProvider = classProvider; + this.configuration = configuration; + } + + @Override + public List resolveMigrations(Context context) { + List migrations = new ArrayList<>(); + + for (Class clazz : classProvider.getClasses()) { + JavaMigration javaMigration = ClassUtils.instantiate(clazz.getName(), configuration.getClassLoader()); + migrations.add(new ResolvedJavaMigration(javaMigration)); + } + + Collections.sort(migrations, new ResolvedMigrationComparator()); + return migrations; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/package-info.java new file mode 100644 index 00000000..b16d4201 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resolver.java; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/package-info.java new file mode 100644 index 00000000..ad9440f8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resolver; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/DefaultSqlMigrationExecutorFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/DefaultSqlMigrationExecutorFactory.java new file mode 100644 index 00000000..0ad3e6a0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/DefaultSqlMigrationExecutorFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.sql; + +public class DefaultSqlMigrationExecutorFactory implements SqlMigrationExecutorFactory { + @Override + public SqlMigrationExecutor createSqlMigrationExecutor() { + return null; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutor.java new file mode 100644 index 00000000..5f06b852 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.sql; + +import org.flywaydb.core.api.executor.Context; +import org.flywaydb.core.api.executor.MigrationExecutor; +import org.flywaydb.core.internal.database.DatabaseExecutionStrategy; +import org.flywaydb.core.internal.database.DatabaseFactory; +import org.flywaydb.core.internal.database.cockroachdb.CockroachDBRetryingStrategy; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.util.SqlCallable; + +import java.sql.SQLException; + +/** + * Database migration based on a sql file. + */ +public class SqlMigrationExecutor implements MigrationExecutor { + private final SqlScriptExecutorFactory sqlScriptExecutorFactory; + + /** + * The SQL script that will be executed. + */ + private final SqlScript sqlScript; + + + + + + + + + + + + + + /** + * Creates a new sql script migration based on this sql script. + * + * @param sqlScript The SQL script that will be executed. + */ + SqlMigrationExecutor(SqlScriptExecutorFactory sqlScriptExecutorFactory, SqlScript sqlScript + + + + ) { + this.sqlScriptExecutorFactory = sqlScriptExecutorFactory; + this.sqlScript = sqlScript; + + + + + } + + @Override + public void execute(final Context context) throws SQLException { + DatabaseExecutionStrategy strategy = DatabaseFactory.createExecutionStrategy(context.getConnection()); + strategy.execute(new SqlCallable() { + @Override + public Boolean call() throws SQLException { + executeOnce(context); + return true; + } + }); + } + + private void executeOnce(Context context) { + sqlScriptExecutorFactory.createSqlScriptExecutor(context.getConnection() + + + + ).execute(sqlScript); + } + + @Override + public boolean canExecuteInTransaction() { + return sqlScript.executeInTransaction(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutorFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutorFactory.java new file mode 100644 index 00000000..06f4e895 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutorFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.sql; + +public interface SqlMigrationExecutorFactory { + SqlMigrationExecutor createSqlMigrationExecutor(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationResolver.java new file mode 100644 index 00000000..d5901d9c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationResolver.java @@ -0,0 +1,223 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resolver.sql; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.resolver.Context; +import org.flywaydb.core.api.resolver.MigrationResolver; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.core.internal.parser.PlaceholderReplacingReader; +import org.flywaydb.core.internal.resolver.ChecksumCalculator; +import org.flywaydb.core.internal.resolver.ResolvedMigrationComparator; +import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceName; +import org.flywaydb.core.internal.resource.ResourceNameParser; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.SqlScript; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; + +import java.io.Reader; +import java.util.*; + +/** + * Migration resolver for SQL files on the classpath. The SQL files must have names like + * V1__Description.sql, V1_1__Description.sql or R__description.sql. + */ +public class SqlMigrationResolver implements MigrationResolver { + /** + * The SQL script executor factory. + */ + private final SqlScriptExecutorFactory sqlScriptExecutorFactory; + + /** + * The resource provider to use. + */ + private final ResourceProvider resourceProvider; + + private final SqlScriptFactory sqlScriptFactory; + + /** + * The Flyway configuration. + */ + private final Configuration configuration; + + private final ParsingContext parsingContext; + + /** + * Creates a new instance. + * + * @param resourceProvider The Scanner for loading migrations on the classpath. + * @param sqlScriptExecutorFactory The SQL script executor factory. + * @param sqlScriptFactory The SQL script factory. + * @param configuration The Flyway configuration. + * @param parsingContext The parsing context. + */ + public SqlMigrationResolver(ResourceProvider resourceProvider, + SqlScriptExecutorFactory sqlScriptExecutorFactory, SqlScriptFactory sqlScriptFactory, + Configuration configuration, ParsingContext parsingContext) { + this.sqlScriptExecutorFactory = sqlScriptExecutorFactory; + this.resourceProvider = resourceProvider; + this.sqlScriptFactory = sqlScriptFactory; + this.configuration = configuration; + this.parsingContext = parsingContext; + } + + public List resolveMigrations(Context context) { + List migrations = new ArrayList<>(); + + String separator = configuration.getSqlMigrationSeparator(); + String[] suffixes = configuration.getSqlMigrationSuffixes(); + addMigrations(migrations, configuration.getSqlMigrationPrefix(), separator, suffixes, + false + + + + ); + + + + + addMigrations(migrations, configuration.getRepeatableSqlMigrationPrefix(), separator, suffixes, + true + + + + ); + + Collections.sort(migrations, new ResolvedMigrationComparator()); + return migrations; + } + + private LoadableResource[] createPlaceholderReplacingLoadableResources(List loadableResources) { + List list = new ArrayList<>(); + + for (final LoadableResource loadableResource : loadableResources) { + LoadableResource placeholderReplacingLoadableResource = new LoadableResource() { + @Override + public Reader read() { + return PlaceholderReplacingReader.create( + configuration, + parsingContext, + loadableResource.read()); + } + + @Override + public String getAbsolutePath() { return loadableResource.getAbsolutePath(); } + @Override + public String getAbsolutePathOnDisk() { return loadableResource.getAbsolutePathOnDisk(); } + @Override + public String getFilename() { return loadableResource.getFilename(); } + @Override + public String getRelativePath() { return loadableResource.getRelativePath(); } + }; + + list.add(placeholderReplacingLoadableResource); + } + + return list.toArray(new LoadableResource[0]); + } + + private Integer getChecksumForLoadableResource(boolean repeatable, List loadableResources) { + if (repeatable && configuration.isPlaceholderReplacement()) { + return ChecksumCalculator.calculate(createPlaceholderReplacingLoadableResources(loadableResources)); + } + + return ChecksumCalculator.calculate(loadableResources.toArray(new LoadableResource[0])); + } + + private Integer getEquivalentChecksumForLoadableResource(boolean repeatable, List loadableResources) { + if (repeatable) { + return ChecksumCalculator.calculate(loadableResources.toArray(new LoadableResource[0])); + } + + return null; + } + + private void addMigrations(List migrations, String prefix, + String separator, String[] suffixes, boolean repeatable + + + + ){ + ResourceNameParser resourceNameParser = new ResourceNameParser(configuration); + + for (LoadableResource resource : resourceProvider.getResources(prefix, suffixes)) { + String filename = resource.getFilename(); + ResourceName result = resourceNameParser.parse(filename); + if (!result.isValid() || isSqlCallback(result) || !prefix.equals(result.getPrefix())) { + continue; + } + + SqlScript sqlScript = sqlScriptFactory.createSqlScript(resource, configuration.isMixed(), resourceProvider); + + List resources = new ArrayList<>(); + resources.add(resource); + + + + + + + + + Integer checksum = getChecksumForLoadableResource(repeatable, resources); + Integer equivalentChecksum = getEquivalentChecksumForLoadableResource(repeatable, resources); + + migrations.add(new ResolvedMigrationImpl( + result.getVersion(), + result.getDescription(), + resource.getRelativePath(), + checksum, + equivalentChecksum, + + + + MigrationType.SQL, + resource.getAbsolutePathOnDisk(), + new SqlMigrationExecutor(sqlScriptExecutorFactory, sqlScript + + + + )) { + @Override + public void validate() { + // Do nothing by default. + } + }); + } + } + + + + /** + * Checks whether this filename is actually a sql-based callback instead of a regular migration. + * + * @param result The parsing result to check. + * @return {@code true} if it is, {@code false} if it isn't. + */ + /* private -> testing */ + static boolean isSqlCallback(ResourceName result) { + if (Event.fromId(result.getPrefix()) != null) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/package-info.java new file mode 100644 index 00000000..34d5edd0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resolver.sql; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/LoadableResource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/LoadableResource.java new file mode 100644 index 00000000..1e3e9644 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/LoadableResource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.util.BomFilter; +import org.flywaydb.core.internal.util.IOUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.zip.CRC32; + +/** + * A loadable resource. + */ +public abstract class LoadableResource implements Resource, Comparable { + + private Integer checksum; + + /** + * Reads the contents of this resource. + * + * @return The reader with the contents of the resource. + */ + public abstract Reader read(); + + + + + + + + + + + + + @Override + public int compareTo(LoadableResource o) { + return getRelativePath().compareTo(o.getRelativePath()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.java new file mode 100644 index 00000000..94c325ef --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import java.util.Collection; +import java.util.Collections; + +/** + * No-op resource provider. + */ +public enum NoopResourceProvider implements ResourceProvider { + INSTANCE; + + @Override + public LoadableResource getResource(String name) { + return null; + } + + /** + * Retrieve all resources whose name begins with this prefix and ends with any of these suffixes. + * + * @param prefix The prefix. + * @param suffixes The suffixes. + * @return The matching resources. + */ + public Collection getResources(String prefix, String[] suffixes) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/Resource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/Resource.java new file mode 100644 index 00000000..be46ccbb --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/Resource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +/** + * A resource (such as a .sql file) used by Flyway. + */ +public interface Resource { + /** + * @return The absolute path and filename of the resource on the classpath or filesystem (path and filename). + */ + String getAbsolutePath(); + + /** + * @return The absolute path and filename of this resource on disk, regardless of whether this resources + * points at the classpath or filesystem. + */ + String getAbsolutePathOnDisk(); + + /** + * @return The filename of this resource, without the path. + */ + String getFilename(); + + /** + * @return The filename of this resource, as well as the path relative to the location where the resource was + * loaded from. + */ + String getRelativePath(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceName.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceName.java new file mode 100644 index 00000000..f8fa7057 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceName.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; + +/** + * Represents a resource name, parsed into its components. + * + * Versioned and Undo migrations are named in the form prefixVERSIONseparatorDESCRIPTIONsuffix; + * Repeatable migrations and callbacks are named in the form prefixSeparatorDESCRIPTIONsuffix + */ +public class ResourceName { + private String prefix; + private String version; + private String separator; + private String description; + private String suffix; + private boolean isValid; + private String validityMessage; + + public ResourceName(String prefix, String version, String separator, String description, String suffix, + boolean isValid, String validityMessage){ + this.prefix = prefix; + this.version = version; + this.separator = separator; + this.description = description; + this.suffix = suffix; + this.isValid = isValid; + this.validityMessage = validityMessage; + } + + /** + * Construct a result representing an invalid resource name + * + * @param message A message explaining the reason the resource name is invalid + * @return The fully populated parsing result. + */ + public static ResourceName invalid(String message) { + return new ResourceName(null, null, null, null, + null, false, message); + } + + /** + * The prefix of the resource (eg. "V" for versioned migrations) + */ + public String getPrefix() { + if (!isValid) { + throw new FlywayException("Cannot access prefix of invalid ResourceNameParseResult\r\n" + validityMessage); + } + return prefix; + } + + private boolean isVersioned() { + return (!"".equals(version)); + } + + /** + * The version of the resource (eg. "1.2.3" for versioned migrations), or null for non-versioned + * resources + */ + public MigrationVersion getVersion() { + if (isVersioned()) { + return MigrationVersion.fromVersion(version); + } else { + return null; + } + } + + /** + * The description of the resource + */ + public String getDescription() { + if (!isValid) { + throw new FlywayException("Cannot access description of invalid ResourceNameParseResult\r\n" + validityMessage); + } + return description; + } + + /** + * The file type suffix of the resource (eg. ".sql" for SQL migration scripts) + */ + public String getSuffix() { + if (!isValid) { + throw new FlywayException("Cannot access suffix of invalid ResourceNameParseResult\r\n" + validityMessage); + } + return suffix; + } + + /** + * The full name of the resource + */ + public String getFilenameWithoutSuffix() { + if (!isValid) { + throw new FlywayException("Cannot access name of invalid ResourceNameParseResult\r\n" + validityMessage); + } + + if ("".equals(description)) { + return prefix + version; + } else { + return prefix + version + separator + description; + } + } + + /** + * Whether the resource name was successfully parsed. + */ + public boolean isValid() { + return isValid; + } + + /** + * If the resource name was not successfully parsed, an explanation of the problem. + */ + public String getValidityMessage() { + return validityMessage; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java new file mode 100644 index 00000000..69e1e6b0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java @@ -0,0 +1,149 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.util.Pair; + +import java.util.*; + +public class ResourceNameParser { + private final Configuration configuration; + private final List> prefixes; + + public ResourceNameParser(Configuration configuration) { + this.configuration = configuration; + // Versioned and Undo migrations are named in the form prefixVERSIONseparatorDESCRIPTIONsuffix + // Repeatable migrations and callbacks are named in the form prefixSeparatorDESCRIPTIONsuffix + prefixes = populatePrefixes(configuration); + } + + public ResourceName parse(String resourceName) { + // Strip off suffixes + Pair suffixResult = stripSuffix(resourceName, configuration.getSqlMigrationSuffixes()); + + // Find the appropriate prefix + Pair prefix = findPrefix(suffixResult.getLeft(), prefixes); + if (prefix != null) { + + // Strip off prefix + Pair prefixResult = stripPrefix(suffixResult.getLeft(), prefix.getLeft()); + String name = prefixResult.getRight(); + Pair splitName = splitAtSeparator(name, configuration.getSqlMigrationSeparator()); + boolean isValid = true; + String validationMessage = ""; + String exampleDescription = ("".equals(splitName.getRight())) ? "description" : splitName.getRight(); + + // Validate the name + if (!ResourceType.isVersioned(prefix.getRight())) { + // Must not have a version (that is, something before the separator) + if (!"".equals(splitName.getLeft())) { + isValid = false; + validationMessage = "Invalid repeatable migration / callback name format: " + resourceName + + " (It cannot contain a version and should look like this: " + + prefixResult.getLeft() + configuration.getSqlMigrationSeparator() + exampleDescription + suffixResult.getRight() + ")"; + } + } else { + // Must have a version (that is, something before the separator) + if ("".equals(splitName.getLeft())) { + isValid = false; + validationMessage = "Invalid versioned migration name format: " + resourceName + + " (It must contain a version and should look like this: " + + prefixResult.getLeft() + "1.2" + configuration.getSqlMigrationSeparator() + exampleDescription + suffixResult.getRight() + ")"; + } else { + // ... and that must be a legitimate version + try { + MigrationVersion.fromVersion(splitName.getLeft()); + } catch (Exception e) { + isValid = false; + validationMessage = "Invalid versioned migration name format: " + resourceName + + " (could not recognise version number " + splitName.getLeft() + ")"; + } + } + } + + String description = splitName.getRight().replace("_", " "); + return new ResourceName(prefixResult.getLeft(), splitName.getLeft(), + configuration.getSqlMigrationSeparator(), description, suffixResult.getRight(), + isValid, validationMessage); + } + + // Didn't match any prefix + return ResourceName.invalid("Unrecognised migration name format: " + resourceName); + } + + private Pair findPrefix(String nameWithoutSuffix, List> prefixes) { + for (Pair prefix: prefixes) { + if (nameWithoutSuffix.startsWith(prefix.getLeft())) { + return prefix; + } + } + return null; + } + + private Pair stripSuffix(String name, String[] suffixes) { + for (String suffix : suffixes) { + if (name.endsWith(suffix)) { + return Pair.of(name.substring(0, name.length() - suffix.length()), suffix); + } + } + return Pair.of(name, ""); + } + + private Pair stripPrefix(String fileName, String prefix) { + if (fileName.startsWith(prefix)) { + return Pair.of(prefix, fileName.substring(prefix.length())); + } + return null; + } + + private Pair splitAtSeparator(String name, String separator) { + int separatorIndex = name.indexOf(separator); + if (separatorIndex >= 0) { + return Pair.of(name.substring(0, separatorIndex), + name.substring(separatorIndex + separator.length())); + } else { + return Pair.of(name, ""); + } + } + + private List> populatePrefixes(Configuration configuration) { + List> prefixes = new ArrayList<>(); + + List versionedPrefixes = new ArrayList<>(); + prefixes.add(Pair.of(configuration.getSqlMigrationPrefix(), ResourceType.MIGRATION)); + + + + prefixes.add(Pair.of(configuration.getRepeatableSqlMigrationPrefix(), ResourceType.REPEATABLE_MIGRATION)); + for (Event event : Event.values()) { + prefixes.add(Pair.of(event.getId(), ResourceType.CALLBACK)); + } + + Comparator> prefixComparator + = new Comparator>() { + public int compare(Pair p1, Pair p2) { + // Sort most-hard-to-match first; that is, in descending order of prefix length + return p2.getLeft().length() - p1.getLeft().length(); + } + }; + + Collections.sort(prefixes, prefixComparator); + return prefixes; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.java new file mode 100644 index 00000000..6c746b53 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ResourceNameValidator { + private static final Log LOG = LogFactory.getLog(ResourceNameValidator.class); + + /** + * Validates the names of all SQL resources returned by the ResourceProvider + * @param provider The ResourceProvider to validate + * @param configuration The configuration to use + */ + public void validateSQLMigrationNaming(ResourceProvider provider, Configuration configuration) { + + List errorsFound = new ArrayList<>(); + ResourceNameParser resourceNameParser = new ResourceNameParser(configuration); + + for (Resource resource : getAllSqlResources(provider, configuration)) { + LOG.debug("Validating " + resource.getFilename()); + + ResourceName result = resourceNameParser.parse(resource.getFilename()); + if (!result.isValid()) { + errorsFound.add(result.getValidityMessage()); + } + } + + if (!errorsFound.isEmpty()) { + throw new FlywayException("Invalid SQL filenames found:\r\n" + StringUtils.collectionToDelimitedString(errorsFound, "\r\n")); + } + } + + private Collection getAllSqlResources(ResourceProvider provider, Configuration configuration) { + return provider.getResources("", configuration.getSqlMigrationSuffixes()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceProvider.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceProvider.java new file mode 100644 index 00000000..46a6aa7d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import java.util.Collection; + +/** + * A facility to obtain loadable resources. + */ +public interface ResourceProvider { + /** + * Retrieves the resource with this name. + * + * @param name The name of the resource. + * @return The resource or {@code null} if not found. + */ + LoadableResource getResource(String name); + + /** + * Retrieve all resources whose name begins with this prefix and ends with any of these suffixes. + * + * @param prefix The prefix. + * @param suffixes The suffixes. + * @return The matching resources. + */ + Collection getResources(String prefix, String[] suffixes); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceType.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceType.java new file mode 100644 index 00000000..8ceb06a1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceType.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +public enum ResourceType { + MIGRATION, + + + + REPEATABLE_MIGRATION, + CALLBACK; + + /** + * Whether the given resource type represents a resource that is versioned. + */ + public static boolean isVersioned(ResourceType type) { + return (type == ResourceType.MIGRATION + + + + ); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/StringResource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/StringResource.java new file mode 100644 index 00000000..f1bcf88c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/StringResource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource; + +import java.io.Reader; +import java.io.StringReader; + +public class StringResource extends LoadableResource { + private final String str; + + public StringResource(String str) { + this.str = str; + } + + @Override + public Reader read() { + return new StringReader(str); + } + + @Override + public String getAbsolutePath() { + return ""; + } + + @Override + public String getAbsolutePathOnDisk() { + return ""; + } + + @Override + public String getFilename() { + return ""; + } + + @Override + public String getRelativePath() { + return ""; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/AndroidResource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/AndroidResource.java new file mode 100644 index 00000000..b4f4c38a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/AndroidResource.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource.android; + +import android.content.res.AssetManager; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.internal.resource.LoadableResource; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +/** + * Resource within an Android App. + */ +public class AndroidResource extends LoadableResource { + private final AssetManager assetManager; + private final String fileName; + private final String fileNameWithAbsolutePath; + private final String fileNameWithRelativePath; + private final Charset encoding; + + public AndroidResource(Location location, AssetManager assetManager, String path, String name, Charset encoding) { + this.assetManager = assetManager; + this.fileNameWithAbsolutePath = path + "/" + name; + this.fileName = name; + this.fileNameWithRelativePath = location == null ? fileNameWithAbsolutePath : location.getPathRelativeToThis(fileNameWithAbsolutePath); + this.encoding = encoding; + } + + @Override + public String getRelativePath() { + return fileNameWithRelativePath; + } + + @Override + public String getAbsolutePath() { + return fileNameWithAbsolutePath; + } + + @Override + public String getAbsolutePathOnDisk() { + return null; + } + + @Override + public Reader read() { + try { + return new InputStreamReader(assetManager.open(fileNameWithAbsolutePath), encoding.newDecoder()); + } catch (IOException e) { + throw new FlywayException("Unable to read asset: " + getAbsolutePath(), e); + } + } + + @Override + public String getFilename() { + return fileName; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/package-info.java new file mode 100644 index 00000000..51d08eff --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resource.android; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/ClassPathResource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/ClassPathResource.java new file mode 100644 index 00000000..41cfcb8a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/ClassPathResource.java @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource.classpath; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.util.UrlUtils; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * A resource on the classpath. + */ +public class ClassPathResource extends LoadableResource { + /** + * The fileNameWithAbsolutePath of the resource on the classpath. + */ + private final String fileNameWithAbsolutePath; + private final String fileNameWithRelativePath; + + /** + * The ClassLoader to use. + */ + private final ClassLoader classLoader; + private final Charset encoding; + + /** + * Creates a new ClassPathResource. + * + * @param fileNameWithAbsolutePath The path and filename of the resource on the classpath. + * @param classLoader The ClassLoader to use. + */ + public ClassPathResource(Location location, String fileNameWithAbsolutePath, ClassLoader classLoader, + Charset encoding) { + this.fileNameWithAbsolutePath = fileNameWithAbsolutePath; + this.fileNameWithRelativePath = location == null ? fileNameWithAbsolutePath : location.getPathRelativeToThis(fileNameWithAbsolutePath); + this.classLoader = classLoader; + this.encoding = encoding; + } + + @Override + public String getRelativePath() { + return fileNameWithRelativePath; + } + + @Override + public String getAbsolutePath() { + return fileNameWithAbsolutePath; + } + + @Override + public String getAbsolutePathOnDisk() { + URL url = getUrl(); + if (url == null) { + throw new FlywayException("Unable to find resource on disk: " + fileNameWithAbsolutePath); + } + return new File(UrlUtils.decodeURL(url.getPath())).getAbsolutePath(); + } + + /** + * @return The url of this resource. + */ + private URL getUrl() { + return classLoader.getResource(fileNameWithAbsolutePath); + } + + @Override + public Reader read() { + InputStream inputStream = classLoader.getResourceAsStream(fileNameWithAbsolutePath); + if (inputStream == null) { + throw new FlywayException("Unable to obtain inputstream for resource: " + fileNameWithAbsolutePath); + } + return new InputStreamReader(inputStream, encoding.newDecoder()); + } + + @Override + public String getFilename() { + return fileNameWithAbsolutePath.substring(fileNameWithAbsolutePath.lastIndexOf("/") + 1); + } + + public boolean exists() { + return getUrl() != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClassPathResource that = (ClassPathResource) o; + + return fileNameWithAbsolutePath.equals(that.fileNameWithAbsolutePath); + } + + @Override + public int hashCode() { + return fileNameWithAbsolutePath.hashCode(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/package-info.java new file mode 100644 index 00000000..d83416d0 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resource.classpath; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/FileSystemResource.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/FileSystemResource.java new file mode 100644 index 00000000..ff9e8a7a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/FileSystemResource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.resource.filesystem; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.util.BomStrippingReader; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.file.StandardOpenOption; + +/** + * A resource on the filesystem. + */ +public class FileSystemResource extends LoadableResource { + + private static final Log LOG = LogFactory.getLog(FileSystemResource.class); + + + + + + + + + + /** + * The location of the resource on the filesystem. + */ + private final File file; + private final String relativePath; + private final Charset encoding; + + + + + /** + * Creates a new ClassPathResource. + * + * @param fileNameWithPath The path and filename of the resource on the filesystem. + */ + public FileSystemResource(Location location, String fileNameWithPath, Charset encoding + + + + ) { + this.file = new File(new File(fileNameWithPath).getPath()); + this.relativePath = location == null ? file.getPath() : location.getPathRelativeToThis(file.getPath()).replace("\\", "/"); + this.encoding = encoding; + + + + } + + /** + * @return The location of the resource on the filesystem. + */ + @Override + public String getAbsolutePath() { + return file.getPath(); + } + + /** + * Retrieves the location of this resource on disk. + * + * @return The location of this resource on disk. + */ + @Override + public String getAbsolutePathOnDisk() { + return file.getAbsolutePath(); + } + + @Override + public Reader read() { + try { + return Channels.newReader(FileChannel.open(file.toPath(), StandardOpenOption.READ), encoding.newDecoder(), 4096); + } catch (IOException e){ + LOG.debug("Unable to load filesystem resource" + file.getPath() + " using FileChannel.open." + + " Falling back to FileInputStream implementation. Exception message: " + e.getMessage()); + } + + try { + return new BufferedReader(new BomStrippingReader(new InputStreamReader(new FileInputStream(file), encoding))); + } catch (IOException e) { + throw new FlywayException("Unable to load filesystem resource: " + file.getPath() + " (encoding: " + encoding + ")", e); + } + } + + + + + + + + + + + + + /** + * @return The filename of this resource, without the path. + */ + @Override + public String getFilename() { + return file.getName(); + } + + @Override + public String getRelativePath() { + return relativePath; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/package-info.java new file mode 100644 index 00000000..8c9fe25e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resource.filesystem; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/package-info.java new file mode 100644 index 00000000..2a25cbc6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.resource; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/LocationScannerCache.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/LocationScannerCache.java new file mode 100644 index 00000000..8b1950cc --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/LocationScannerCache.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner; + +import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class LocationScannerCache { + + /** + * Cache the location scanner for each protocol. + */ + private final Map cache = new HashMap<>(); + + public boolean containsKey(String protocol) { + return cache.containsKey(protocol); + } + + public ClassPathLocationScanner get(String protocol) { + return cache.get(protocol); + } + + public void put(String protocol, ClassPathLocationScanner scanner) { + cache.put(protocol, scanner); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/ResourceNameCache.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/ResourceNameCache.java new file mode 100644 index 00000000..a4c99c02 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/ResourceNameCache.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner; + +import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class ResourceNameCache { + /** + * Cache resource names. + */ + private final Map>> resourceNameCache = new HashMap<>(); + + public void put(ClassPathLocationScanner classPathLocationScanner, Map> map){ + resourceNameCache.put(classPathLocationScanner, map); + } + + public void put(ClassPathLocationScanner classPathLocationScanner, URL resolvedUrl, Set names){ + resourceNameCache.get(classPathLocationScanner).put(resolvedUrl, names); + } + + public Set get(ClassPathLocationScanner classPathLocationScanner, URL resolvedUrl){ + return resourceNameCache.get(classPathLocationScanner).get(resolvedUrl); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java new file mode 100644 index 00000000..fdc2321c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java @@ -0,0 +1,115 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.clazz.ClassProvider; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceProvider; +import org.flywaydb.core.internal.scanner.android.AndroidScanner; +import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner; +import org.flywaydb.core.internal.scanner.classpath.ClassPathScanner; +import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner; +import org.flywaydb.core.internal.scanner.filesystem.FileSystemScanner; +import org.flywaydb.core.internal.util.FeatureDetector; +import org.flywaydb.core.internal.util.StringUtils; + +import java.nio.charset.Charset; +import java.util.*; + +/** + * Scanner for Resources and Classes. + */ +public class Scanner implements ResourceProvider, ClassProvider { + private static final Log LOG = LogFactory.getLog(Scanner.class); + + private final List resources = new ArrayList<>(); + private final List> classes = new ArrayList<>(); + + /* + * Constructor. Scans the given locations for resources, and classes implementing the specified interface. + */ + public Scanner(Class implementedInterface, Collection locations, ClassLoader classLoader, Charset encoding + + + + , ResourceNameCache resourceNameCache + , LocationScannerCache locationScannerCache + ) { + FileSystemScanner fileSystemScanner = new FileSystemScanner(encoding + + + + ); + + boolean android = new FeatureDetector(classLoader).isAndroidAvailable(); + + for (Location location : locations) { + if (location.isFileSystem()) { + resources.addAll(fileSystemScanner.scanForResources(location)); + } else { + ResourceAndClassScanner resourceAndClassScanner = android + ? new AndroidScanner<>(implementedInterface, classLoader, encoding, location) + : new ClassPathScanner<>(implementedInterface, classLoader, encoding, location, resourceNameCache, locationScannerCache); + resources.addAll(resourceAndClassScanner.scanForResources()); + classes.addAll(resourceAndClassScanner.scanForClasses()); + } + } + } + + @Override + public LoadableResource getResource(String name) { + for (LoadableResource resource : resources) { + String fileName = resource.getRelativePath(); + if (fileName.equals(name)) { + return resource; + } + } + return null; + } + + /** + * Returns all known resources starting with the specified prefix and ending with any of the specified suffixes. + * + * @param prefix The prefix of the resource names to match. + * @param suffixes The suffixes of the resource names to match. + * @return The resources that were found. + */ + public Collection getResources(String prefix, String... suffixes) { + List result = new ArrayList<>(); + for (LoadableResource resource : resources) { + String fileName = resource.getFilename(); + if (StringUtils.startsAndEndsWith(fileName, prefix, suffixes)) { + result.add(resource); + } else { + LOG.debug("Filtering out resource: " + resource.getAbsolutePath() + " (filename: " + fileName + ")"); + } + } + return result; + } + + /** + * Scans the classpath for concrete classes under the specified package implementing the specified interface. + * Non-instantiable abstract classes are filtered out. + * + * @return The non-abstract classes that were found. + */ + public Collection> getClasses() { + return Collections.unmodifiableCollection(classes); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/AndroidScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/AndroidScanner.java new file mode 100644 index 00000000..3a7befea --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/AndroidScanner.java @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.android; + +import android.content.Context; +import dalvik.system.DexFile; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.android.ContextHolder; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.android.AndroidResource; +import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner; +import org.flywaydb.core.internal.util.ClassUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; + +/** + * Class & resource scanner for Android. + */ +public class AndroidScanner implements ResourceAndClassScanner { + private static final Log LOG = LogFactory.getLog(AndroidScanner.class); + + private final Context context; + + private final Class implementedInterface; + private final ClassLoader clazzLoader; + private final Charset encoding; + private final Location location; + + public AndroidScanner(Class implementedInterface, ClassLoader clazzLoader, Charset encoding, Location location) { + this.implementedInterface = implementedInterface; + this.clazzLoader = clazzLoader; + this.encoding = encoding; + this.location = location; + context = ContextHolder.getContext(); + if (context == null) { + throw new FlywayException("Unable to scan for Migrations! Context not set. " + + "Within an activity you can fix this with org.flywaydb.core.api.android.ContextHolder.setContext(this);"); + } + } + + @Override + public Collection scanForResources() { + List resources = new ArrayList<>(); + + String path = location.getRootPath(); + try { + for (String asset : context.getAssets().list(path)) { + if (location.matchesPath(asset)) { + resources.add(new AndroidResource(location, context.getAssets(), path, asset, encoding)); + } + } + } catch (IOException e) { + LOG.warn("Unable to scan for resources: " + e.getMessage()); + } + + return resources; + } + + @Override + public Collection> scanForClasses() { + String pkg = location.getRootPath().replace("/", "."); + + List> classes = new ArrayList<>(); + String sourceDir = context.getApplicationInfo().sourceDir; + DexFile dex = null; + try { + dex = new DexFile(sourceDir); + Enumeration entries = dex.entries(); + while (entries.hasMoreElements()) { + String className = entries.nextElement(); + if (className.startsWith(pkg)) { + Class clazz = ClassUtils.loadClass(implementedInterface, className, clazzLoader); + if (clazz != null) { + classes.add(clazz); + } + } + } + } catch (IOException e) { + LOG.warn("Unable to scan DEX file (" + sourceDir + "): " + e.getMessage()); + } finally { + if (dex != null) { + try { + dex.close(); + } catch (IOException e) { + LOG.debug("Unable to close DEX file (" + sourceDir + "): " + e.getMessage()); + } + } + } + return classes; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/package-info.java new file mode 100644 index 00000000..a617bdb3 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.scanner.android; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathLocationScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathLocationScanner.java new file mode 100644 index 00000000..8ac71d89 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathLocationScanner.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import java.net.URL; +import java.util.Set; + +/** + * Scans for classpath resources in this location. + */ +public interface ClassPathLocationScanner { + /** + * Finds the resource names below this location on the classpath under this locationUrl. + * + * @param location The system-independent location on the classpath. + * @param locationUrl The system-specific physical location URL. + * @return The system-independent names of the resources on the classpath. + */ + Set findResourceNames(String location, URL locationUrl); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathScanner.java new file mode 100644 index 00000000..76b74db9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathScanner.java @@ -0,0 +1,349 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.classpath.ClassPathResource; +import org.flywaydb.core.internal.scanner.LocationScannerCache; +import org.flywaydb.core.internal.scanner.ResourceNameCache; +import org.flywaydb.core.internal.scanner.classpath.jboss.JBossVFSv2UrlResolver; +import org.flywaydb.core.internal.scanner.classpath.jboss.JBossVFSv3ClassPathLocationScanner; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.FeatureDetector; +import org.flywaydb.core.internal.util.UrlUtils; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; + +/** + * ClassPath scanner. + */ +public class ClassPathScanner implements ResourceAndClassScanner { + private static final Log LOG = LogFactory.getLog(ClassPathScanner.class); + + private final Class implementedInterface; + /** + * The ClassLoader for loading migrations on the classpath. + */ + private final ClassLoader classLoader; + private final Location location; + + private final Set resources = new TreeSet<>(); + + /** + * Cache location lookups. + */ + private final Map> locationUrlCache = new HashMap<>(); + + /** + * Cache location scanners. + */ + private final LocationScannerCache locationScannerCache; + + /** + * Cache resource names. + */ + private final ResourceNameCache resourceNameCache; + + /** + * Creates a new Classpath scanner. + * + * @param classLoader The ClassLoader for loading migrations on the classpath. + */ + public ClassPathScanner(Class implementedInterface, ClassLoader classLoader, Charset encoding, Location location, + ResourceNameCache resourceNameCache, + LocationScannerCache locationScannerCache) { + this.implementedInterface = implementedInterface; + this.classLoader = classLoader; + this.location = location; + this.resourceNameCache = resourceNameCache; + this.locationScannerCache = locationScannerCache; + + LOG.debug("Scanning for classpath resources at '" + location + "' ..."); + for (String resourceName : findResourceNames()) { + resources.add(new ClassPathResource(location, resourceName, classLoader, encoding)); + LOG.debug("Found resource: " + resourceName); + } + } + + @Override + public Collection scanForResources() { + return resources; + } + + @Override + public Collection> scanForClasses() { + LOG.debug("Scanning for classes at " + location); + + List> classes = new ArrayList<>(); + + for (LoadableResource resource : resources) { + if (resource.getAbsolutePath().endsWith(".class")) { + Class clazz = ClassUtils.loadClass( + implementedInterface, + toClassName(resource.getAbsolutePath()), + classLoader); + if (clazz != null) { + classes.add(clazz); + } + } + } + + return classes; + } + + /** + * Converts this resource name to a fully qualified class name. + * + * @param resourceName The resource name. + * @return The class name. + */ + private String toClassName(String resourceName) { + String nameWithDots = resourceName.replace("/", "."); + return nameWithDots.substring(0, (nameWithDots.length() - ".class".length())); + } + + /** + * Finds the resources names present at this location and below on the classpath starting with this prefix and + * ending with this suffix. + * + * @return The resource names. + */ + private Set findResourceNames() { + Set resourceNames = new TreeSet<>(); + + List locationUrls = getLocationUrlsForPath(location); + for (URL locationUrl : locationUrls) { + LOG.debug("Scanning URL: " + locationUrl.toExternalForm()); + + UrlResolver urlResolver = createUrlResolver(locationUrl.getProtocol()); + URL resolvedUrl = urlResolver.toStandardJavaUrl(locationUrl); + + String protocol = resolvedUrl.getProtocol(); + ClassPathLocationScanner classPathLocationScanner = createLocationScanner(protocol); + if (classPathLocationScanner == null) { + String scanRoot = UrlUtils.toFilePath(resolvedUrl); + LOG.warn("Unable to scan location: " + scanRoot + " (unsupported protocol: " + protocol + ")"); + } else { + Set names = resourceNameCache.get(classPathLocationScanner, resolvedUrl); + if (names == null) { + names = classPathLocationScanner.findResourceNames(location.getRootPath(), resolvedUrl); + resourceNameCache.put(classPathLocationScanner, resolvedUrl, names); + } + Set filteredNames = new HashSet<>(); + for (String name : names) { + if (location.matchesPath(name)) { + filteredNames.add(name); + } + } + + resourceNames.addAll(filteredNames); + } + } + + // Make an additional attempt at finding resources in jar files in case the URL scanning method above didn't + // yield any results. + boolean locationResolved = !locationUrls.isEmpty(); + + // Starting with Java 11, resources at the root of the classpath aren't being found using the URL scanning + // method above and we need to revert to Jar file walking. + boolean isClassPathRoot = location.isClassPath() && "".equals(location.getRootPath()); + + if (!locationResolved || isClassPathRoot) { + if (classLoader instanceof URLClassLoader) { + URLClassLoader urlClassLoader = (URLClassLoader) classLoader; + for (URL url : urlClassLoader.getURLs()) { + if ("file".equals(url.getProtocol()) + && url.getPath().endsWith(".jar") + && !url.getPath().matches(".*" + Pattern.quote("/jre/lib/") + ".*")) { + // All non-system jars on disk + JarFile jarFile; + try { + try { + jarFile = new JarFile(url.toURI().getSchemeSpecificPart()); + } catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + jarFile = new JarFile(url.getPath().substring("file:".length())); + } + } catch (IOException | SecurityException e) { + LOG.warn("Skipping unloadable jar file: " + url + " (" + e.getMessage() + ")"); + continue; + } + + try { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + String entryName = entries.nextElement().getName(); + if (entryName.startsWith(location.getRootPath())) { + locationResolved = true; + resourceNames.add(entryName); + } + } + } finally { + try { + jarFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + } + } + + if (!locationResolved) { + LOG.warn("Unable to resolve location " + location + ". Note this warning will become an error in Flyway 7."); + } + + return resourceNames; + } + + /** + * Gets the physical location urls for this logical path on the classpath. + * + * @param location The location on the classpath. + * @return The underlying physical URLs. + */ + private List getLocationUrlsForPath(Location location) { + if (locationUrlCache.containsKey(location)) { + return locationUrlCache.get(location); + } + + LOG.debug("Determining location urls for " + location + " using ClassLoader " + classLoader + " ..."); + + List locationUrls = new ArrayList<>(); + + if (classLoader.getClass().getName().startsWith("com.ibm")) { + // WebSphere + Enumeration urls; + try { + urls = classLoader.getResources(location.getRootPath() + "/flyway.location"); + if (!urls.hasMoreElements()) { + LOG.warn("Unable to resolve location " + location + " (ClassLoader: " + classLoader + ")" + + " On WebSphere an empty file named flyway.location must be present on the classpath location for WebSphere to find it!\nNote this warning will become an error in Flyway 7."); + } + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + locationUrls.add(new URL(UrlUtils.decodeURL(url.toExternalForm()).replace("/flyway.location", ""))); + } + } catch (IOException e) { + LOG.warn("Unable to resolve location " + location + " (ClassLoader: " + classLoader + ")" + + " On WebSphere an empty file named flyway.location must be present on the classpath location for WebSphere to find it!\nNote this warning will become an error in Flyway 7."); + } + } else { + Enumeration urls; + try { + urls = classLoader.getResources(location.getRootPath()); + while (urls.hasMoreElements()) { + locationUrls.add(urls.nextElement()); + } + } catch (IOException e) { + LOG.warn("Unable to resolve location " + location + " (ClassLoader: " + classLoader + "): " + e.getMessage() + "\nNote this warning will become an error in Flyway 7."); + } + } + + locationUrlCache.put(location, locationUrls); + + return locationUrls; + } + + /** + * Creates an appropriate URL resolver scanner for this url protocol. + * + * @param protocol The protocol of the location url to scan. + * @return The url resolver for this protocol. + */ + private UrlResolver createUrlResolver(String protocol) { + if (new FeatureDetector(classLoader).isJBossVFSv2Available() && protocol.startsWith("vfs")) { + return new JBossVFSv2UrlResolver(); + } + + return new DefaultUrlResolver(); + } + + /** + * Creates an appropriate location scanner for this url protocol. + * + * @param protocol The protocol of the location url to scan. + * @return The location scanner or {@code null} if it could not be created. + */ + private ClassPathLocationScanner createLocationScanner(String protocol) { + if (locationScannerCache.containsKey(protocol)) { + return locationScannerCache.get(protocol); + } + + if ("file".equals(protocol)) { + FileSystemClassPathLocationScanner locationScanner = new FileSystemClassPathLocationScanner(); + locationScannerCache.put(protocol, locationScanner); + resourceNameCache.put(locationScanner, new HashMap<>()); + return locationScanner; + } + + if ("jar".equals(protocol) || isTomcat(protocol) || isWebLogic(protocol) || isWebSphere(protocol)) { + String separator = isTomcat(protocol) ? "*/" : "!/"; + ClassPathLocationScanner locationScanner = new JarFileClassPathLocationScanner(separator); + locationScannerCache.put(protocol, locationScanner); + resourceNameCache.put(locationScanner, new HashMap<>()); + return locationScanner; + } + + FeatureDetector featureDetector = new FeatureDetector(classLoader); + if (featureDetector.isJBossVFSv3Available() && "vfs".equals(protocol)) { + JBossVFSv3ClassPathLocationScanner locationScanner = new JBossVFSv3ClassPathLocationScanner(); + locationScannerCache.put(protocol, locationScanner); + resourceNameCache.put(locationScanner, new HashMap<>()); + return locationScanner; + } + if (featureDetector.isOsgiFrameworkAvailable() && (isFelix(protocol) || isEquinox(protocol))) { + OsgiClassPathLocationScanner locationScanner = new OsgiClassPathLocationScanner(); + locationScannerCache.put(protocol, locationScanner); + resourceNameCache.put(locationScanner, new HashMap<>()); + return locationScanner; + } + + return null; + } + + private boolean isEquinox(String protocol) { + return "bundleresource".equals(protocol); + } + + private boolean isFelix(String protocol) { + return "bundle".equals(protocol); + } + + private boolean isWebSphere(String protocol) { + return "wsjar".equals(protocol); + } + + private boolean isWebLogic(String protocol) { + return "zip".equals(protocol); + } + + private boolean isTomcat(String protocol) { + return "war".equals(protocol); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/DefaultUrlResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/DefaultUrlResolver.java new file mode 100644 index 00000000..26024326 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/DefaultUrlResolver.java @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import java.net.URL; + +/** + * Default implementation of UrlResolver. + */ +public class DefaultUrlResolver implements UrlResolver { + public URL toStandardJavaUrl(URL url) { + return url; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/FileSystemClassPathLocationScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/FileSystemClassPathLocationScanner.java new file mode 100644 index 00000000..0739f3ab --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/FileSystemClassPathLocationScanner.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.UrlUtils; + +import java.io.File; +import java.net.URL; +import java.util.Set; +import java.util.TreeSet; + +/** + * ClassPathLocationScanner for the file system. + */ +public class FileSystemClassPathLocationScanner implements ClassPathLocationScanner { + private static final Log LOG = LogFactory.getLog(FileSystemClassPathLocationScanner.class); + + public Set findResourceNames(String location, URL locationUrl) { + String filePath = UrlUtils.toFilePath(locationUrl); + File folder = new File(filePath); + if (!folder.isDirectory()) { + LOG.debug("Skipping path as it is not a directory: " + filePath); + return new TreeSet<>(); + } + + String classPathRootOnDisk = filePath.substring(0, filePath.length() - location.length()); + if (!classPathRootOnDisk.endsWith(File.separator)) { + classPathRootOnDisk = classPathRootOnDisk + File.separator; + } + LOG.debug("Scanning starting at classpath root in filesystem: " + classPathRootOnDisk); + return findResourceNamesFromFileSystem(classPathRootOnDisk, location, folder); + } + + /** + * Finds all the resource names contained in this file system folder. + * + * @param classPathRootOnDisk The location of the classpath root on disk, with a trailing slash. + * @param scanRootLocation The root location of the scan on the classpath, without leading or trailing slashes. + * @param folder The folder to look for resources under on disk. + * @return The resource names; + */ + /*private -> for testing*/ + @SuppressWarnings("ConstantConditions") + Set findResourceNamesFromFileSystem(String classPathRootOnDisk, String scanRootLocation, File folder) { + LOG.debug("Scanning for resources in path: " + folder.getPath() + " (" + scanRootLocation + ")"); + + Set resourceNames = new TreeSet<>(); + + File[] files = folder.listFiles(); + for (File file : files) { + if (file.canRead()) { + if (file.isDirectory()) { + resourceNames.addAll(findResourceNamesFromFileSystem(classPathRootOnDisk, scanRootLocation, file)); + } else { + resourceNames.add(toResourceNameOnClasspath(classPathRootOnDisk, file)); + } + } + } + + return resourceNames; + } + + /** + * Converts this file into a resource name on the classpath. + * + * @param classPathRootOnDisk The location of the classpath root on disk, with a trailing slash. + * @param file The file. + * @return The resource name on the classpath. + */ + private String toResourceNameOnClasspath(String classPathRootOnDisk, File file) { + String fileName = file.getAbsolutePath().replace("\\", "/"); + + //Cut off the part on disk leading to the root of the classpath + //This leaves a resource name starting with the scanRootLocation, + // with no leading slash, containing subDirs and the fileName. + return fileName.substring(classPathRootOnDisk.length()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java new file mode 100644 index 00000000..4baac1e9 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java @@ -0,0 +1,133 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.util.IOUtils; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * ClassPathLocationScanner for jar files. + */ +public class JarFileClassPathLocationScanner implements ClassPathLocationScanner { + private static final Log LOG = LogFactory.getLog(JarFileClassPathLocationScanner.class); + + /** + * The separator that delimits the jar file name and the file inside the jar within a URL. + */ + private final String separator; + + /** + * @param separator The separator that delimits the jar file name and the file inside the jar within a URL. + */ + JarFileClassPathLocationScanner(String separator) { this.separator = separator; } + + public Set findResourceNames(String location, URL locationUrl) { + JarFile jarFile; + try { + jarFile = getJarFromUrl(locationUrl); + } catch (IOException e) { + LOG.warn("Unable to determine jar from url (" + locationUrl + "): " + e.getMessage()); + return Collections.emptySet(); + } + + try { + // For Tomcat and non-expanded WARs. + String prefix = jarFile.getName().toLowerCase().endsWith(".war") ? "WEB-INF/classes/" : ""; + return findResourceNamesFromJarFile(jarFile, prefix, location); + } finally { + try { + jarFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + + /** + * Retrieves the Jar file represented by this URL. + * + * @param locationUrl The URL of the jar. + * @return The jar file. + * @throws IOException when the jar could not be resolved. + */ + private JarFile getJarFromUrl(URL locationUrl) throws IOException { + URLConnection con = locationUrl.openConnection(); + if (con instanceof JarURLConnection) { + // Should usually be the case for traditional JAR files. + JarURLConnection jarCon = (JarURLConnection) con; + jarCon.setUseCaches(false); + return jarCon.getJarFile(); + } + + // No JarURLConnection -> need to resort to URL file parsing. + // We'll assume URLs of the format "jar:path!/entry", with the protocol + // being arbitrary as long as following the entry format. + // We'll also handle paths with and without leading "file:" prefix. + String urlFile = locationUrl.getFile(); + + int separatorIndex = urlFile.indexOf(separator); + if (separatorIndex != -1) { + String jarFileUrl = urlFile.substring(0, separatorIndex); + if (jarFileUrl.startsWith("file:")) { + try { + return new JarFile(new URL(jarFileUrl).toURI().getSchemeSpecificPart()); + } catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new JarFile(jarFileUrl.substring("file:".length())); + } + } + return new JarFile(jarFileUrl); + } + + return new JarFile(urlFile); + } + + /** + * Finds all the resource names contained in this directory within this jar file. + * + * @param jarFile The jar file. + * @param prefix The prefix to ignore within the jar file. + * @param location The location to look under. + * @return The resource names. + */ + private Set findResourceNamesFromJarFile(JarFile jarFile, String prefix, String location) { + String toScan = prefix + location + (location.endsWith("/") ? "" : "/"); + Set resourceNames = new TreeSet<>(); + + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + String entryName = entries.nextElement().getName(); + if (entryName.startsWith(toScan)) { + resourceNames.add(entryName.substring(prefix.length())); + } + } + + return resourceNames; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarUtils.java new file mode 100644 index 00000000..9cf1cdb4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarUtils.java @@ -0,0 +1,23 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +/** + * Utility methods for working with Jar files. + */ +public class JarUtils { + private JarUtils() {} +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/OsgiClassPathLocationScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/OsgiClassPathLocationScanner.java new file mode 100644 index 00000000..758a5bf6 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/OsgiClassPathLocationScanner.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import org.flywaydb.core.api.FlywayException; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * OSGi specific scanner that performs the migration search in + * the current bundle's classpath. + * + *

+ * The resources that this scanner returns can only be loaded if + * Flyway's ClassLoader has access to the bundle that contains the migrations. + *

+ */ +public class OsgiClassPathLocationScanner implements ClassPathLocationScanner { + // Equinox "host" resource url pattern starts with bundleId, which is long according osgi core specification + private static final Pattern EQUINOX_BUNDLE_ID_PATTERN = Pattern.compile("^\\d+"); + + // #2198: Felix 6.0+ uses a "host" like e3a74e5a-af1f-46f0-bb53-bc5fee1b4a57_145.0 instead, where 145 is the bundle id. + private static final Pattern FELIX_BUNDLE_ID_PATTERN = Pattern.compile("^[0-9a-f\\-]{36}_(\\d+)\\.\\d+"); + + public Set findResourceNames(String location, URL locationUrl) { + Set resourceNames = new TreeSet<>(); + + Bundle bundle = getTargetBundleFromContextOrCurrent(FrameworkUtil.getBundle(getClass()), locationUrl); + Enumeration entries = bundle.findEntries(locationUrl.getPath(), "*", true); + + if (entries != null) { + while (entries.hasMoreElements()) { + URL entry = entries.nextElement(); + String resourceName = getPathWithoutLeadingSlash(entry); + + resourceNames.add(resourceName); + } + } + + return resourceNames; + } + + private Bundle getTargetBundleFromContextOrCurrent(Bundle current, URL locationUrl) { + Bundle target; + try { + target = current.getBundleContext().getBundle(hostToBundleId(locationUrl.getHost())); + } catch (RuntimeException e) { + return current; + } + return target != null ? target : current; + } + + static long hostToBundleId(String host) { + Matcher m = FELIX_BUNDLE_ID_PATTERN.matcher(host); + if (m.find()) { + return Double.valueOf(m.group(1)).longValue(); + } else { + m = EQUINOX_BUNDLE_ID_PATTERN.matcher(host); + if (m.find()) { + return Double.valueOf(m.group()).longValue(); + } + } + throw new FlywayException("There's no bundleId in passed URL"); + } + + private String getPathWithoutLeadingSlash(URL entry) { + final String path = entry.getPath(); + + return path.startsWith("/") ? path.substring(1) : path; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ResourceAndClassScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ResourceAndClassScanner.java new file mode 100644 index 00000000..63115366 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ResourceAndClassScanner.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import org.flywaydb.core.internal.resource.LoadableResource; + +import java.util.Collection; + +/** + * Scanner for both resources and classes. + */ +public interface ResourceAndClassScanner { + /** + * Scans the classpath for resources under the configured location. + * + * @return The resources that were found. + */ + Collection scanForResources(); + + /** + * Scans the classpath for concrete classes under the specified package implementing the specified interface. + * Non-instantiable abstract classes are filtered out. + * + * @return The non-abstract classes that were found. + */ + Collection> scanForClasses(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/UrlResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/UrlResolver.java new file mode 100644 index 00000000..2471074d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/UrlResolver.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath; + +import java.net.URL; + +/** + * Resolves container-specific URLs into standard Java URLs. + */ +public interface UrlResolver { + /** + * Resolves this container-specific URL into standard Java URL. + * + * @param url The URL to resolve. + * @return The matching standard Java URL. + */ + URL toStandardJavaUrl(URL url); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv2UrlResolver.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv2UrlResolver.java new file mode 100644 index 00000000..49fce0e7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv2UrlResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath.jboss; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.scanner.classpath.UrlResolver; + +import java.lang.reflect.Method; +import java.net.URL; + +/** + * Resolves JBoss VFS v2 URLs into standard Java URLs. + */ +public class JBossVFSv2UrlResolver implements UrlResolver { + public URL toStandardJavaUrl(URL url) { + try { + Class vfsClass = Class.forName("org.jboss.virtual.VFS"); + Class vfsUtilsClass = Class.forName("org.jboss.virtual.VFSUtils"); + Class virtualFileClass = Class.forName("org.jboss.virtual.VirtualFile"); + + Method getRootMethod = vfsClass.getMethod("getRoot", URL.class); + Method getRealURLMethod = vfsUtilsClass.getMethod("getRealURL", virtualFileClass); + + Object root = getRootMethod.invoke(null, url); + return (URL) getRealURLMethod.invoke(null, root); + } catch (Exception e) { + throw new FlywayException("JBoss VFS v2 call failed", e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv3ClassPathLocationScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv3ClassPathLocationScanner.java new file mode 100644 index 00000000..c6c6fbf7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv3ClassPathLocationScanner.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.classpath.jboss; + +import org.flywaydb.core.internal.util.UrlUtils; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner; +import org.jboss.vfs.VFS; +import org.jboss.vfs.VirtualFile; +import org.jboss.vfs.VirtualFileFilter; + +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * ClassPathLocationScanner for JBoss VFS v3. + */ +public class JBossVFSv3ClassPathLocationScanner implements ClassPathLocationScanner { + private static final Log LOG = LogFactory.getLog(JBossVFSv3ClassPathLocationScanner.class); + + public Set findResourceNames(String location, URL locationUrl) { + String filePath = UrlUtils.toFilePath(locationUrl); + String classPathRootOnDisk = filePath.substring(0, filePath.length() - location.length()); + if (!classPathRootOnDisk.endsWith("/")) { + classPathRootOnDisk = classPathRootOnDisk + "/"; + } + LOG.debug("Scanning starting at classpath root on JBoss VFS: " + classPathRootOnDisk); + + Set resourceNames = new TreeSet<>(); + + List files; + try { + files = VFS.getChild(filePath).getChildrenRecursively(new VirtualFileFilter() { + public boolean accepts(VirtualFile file) { + return file.isFile(); + } + }); + for (VirtualFile file : files) { + resourceNames.add(file.getPathName().substring(classPathRootOnDisk.length())); + } + } catch (IOException e) { + LOG.warn("Unable to scan classpath root (" + classPathRootOnDisk + ") using JBoss VFS: " + e.getMessage()); + } + + return resourceNames; + } + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/package-info.java new file mode 100644 index 00000000..af277172 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.scanner.classpath.jboss; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/package-info.java new file mode 100644 index 00000000..dbb3124e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.scanner.classpath; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.java new file mode 100644 index 00000000..c46722a5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.scanner.filesystem; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.filesystem.FileSystemResource; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +/** + * FileSystem scanner. + */ +public class FileSystemScanner { + private static final Log LOG = LogFactory.getLog(FileSystemScanner.class); + private final Charset encoding; + + + + + + /** + * Creates a new filesystem scanner. + * + * @param encoding The encoding to use. + + + + */ + public FileSystemScanner(Charset encoding + + + + ) { + this.encoding = encoding; + + + + } + + /** + * Scans the FileSystem for resources under the specified location, starting with the specified prefix and ending with + * the specified suffix. + * + * @param location The location in the filesystem to start searching. Subdirectories are also searched. + * @return The resources that were found. + */ + public Collection scanForResources(Location location) { + String path = location.getRootPath(); + LOG.debug("Scanning for filesystem resources at '" + path + "'"); + + File dir = new File(path); + if (!dir.exists()) { + LOG.warn("Skipping filesystem location:" + path + " (not found). Note this warning will become an error in Flyway 7."); + return Collections.emptyList(); + } + if (!dir.canRead()) { + LOG.warn("Skipping filesystem location:" + path + " (not readable). Note this warning will become an error in Flyway 7."); + return Collections.emptyList(); + } + if (!dir.isDirectory()) { + LOG.warn("Skipping filesystem location:" + path + " (not a directory). Note this warning will become an error in Flyway 7."); + return Collections.emptyList(); + } + + Set resources = new TreeSet<>(); + + for (String resourceName : findResourceNamesFromFileSystem(path, new File(path))) { + + if (location.matchesPath(resourceName)) { + resources.add(new FileSystemResource(location, resourceName, encoding + + + + )); + LOG.debug("Found filesystem resource: " + resourceName); + } + } + + return resources; + } + + /** + * Finds all the resource names contained in this file system folder. + * + * @param scanRootLocation The root location of the scan on disk. + * @param folder The folder to look for resources under on disk. + * @return The resource names; + */ + @SuppressWarnings("ConstantConditions") + private Set findResourceNamesFromFileSystem(String scanRootLocation, File folder) { + LOG.debug("Scanning for resources in path: " + folder.getPath() + " (" + scanRootLocation + ")"); + + Set resourceNames = new TreeSet<>(); + + File[] files = folder.listFiles(); + for (File file : files) { + if (file.canRead()) { + if (file.isDirectory()) { + if (file.isHidden()) { + // #1807: Skip hidden directories to avoid issues with Kubernetes + LOG.debug("Skipping hidden directory: " + file.getAbsolutePath()); + } else { + resourceNames.addAll(findResourceNamesFromFileSystem(scanRootLocation, file)); + } + } else { + resourceNames.add(file.getPath()); + } + } + } + + return resourceNames; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/package-info.java new file mode 100644 index 00000000..84fe547a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.scanner; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/AppliedMigration.java b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/AppliedMigration.java new file mode 100644 index 00000000..b67caad5 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/AppliedMigration.java @@ -0,0 +1,216 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.schemahistory; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; + +import java.util.Date; +import java.util.Objects; + +/** + * A migration applied to the database (maps to a row in the schema history table). + */ +public class AppliedMigration implements Comparable { + /** + * The order in which this migration was applied amongst all others. (For out of order detection) + */ + private final int installedRank; + + /** + * The target version of this migration. {@code null} if it is a repeatable migration. + */ + private final MigrationVersion version; + + /** + * The description of the migration. + */ + private final String description; + + /** + * The type of migration (BASELINE, SQL, ...) + */ + private final MigrationType type; + + /** + * The name of the script to execute for this migration, relative to its classpath location. + */ + private final String script; + + /** + * The checksum of the migration. (Optional) + */ + private final Integer checksum; + + /** + * The timestamp when this migration was installed. + */ + private final Date installedOn; + + /** + * The user that installed this migration. + */ + private final String installedBy; + + /** + * The execution time (in millis) of this migration. + */ + private final int executionTime; + + /** + * Flag indicating whether the migration was successful or not. + */ + private final boolean success; + + /** + * Creates a new applied migration. Only called from the RowMapper. + * + * @param installedRank The order in which this migration was applied amongst all others. (For out of order detection) + * @param version The target version of this migration. + * @param description The description of the migration. + * @param type The type of migration (INIT, SQL, ...) + * @param script The name of the script to execute for this migration, relative to its classpath location. + * @param checksum The checksum of the migration. (Optional) + * @param installedOn The timestamp when this migration was installed. + * @param installedBy The user that installed this migration. + * @param executionTime The execution time (in millis) of this migration. + * @param success Flag indicating whether the migration was successful or not. + */ + public AppliedMigration(int installedRank, MigrationVersion version, String description, + MigrationType type, String script, Integer checksum, Date installedOn, + String installedBy, int executionTime, boolean success) { + this.installedRank = installedRank; + this.version = version; + this.description = description; + this.type = type; + this.script = script; + this.checksum = checksum; + this.installedOn = installedOn; + this.installedBy = installedBy; + this.executionTime = executionTime; + this.success = success; + } + + /** + * @return The order in which this migration was applied amongst all others. (For out of order detection) + */ + public int getInstalledRank() { + return installedRank; + } + + /** + * @return The target version of this migration. + */ + public MigrationVersion getVersion() { + return version; + } + + /** + * @return The description of the migration. + */ + public String getDescription() { + return description; + } + + /** + * @return The type of migration (BASELINE, SQL, ...) + */ + public MigrationType getType() { + return type; + } + + /** + * @return The name of the script to execute for this migration, relative to its classpath location. + */ + public String getScript() { + return script; + } + + /** + * @return The checksum of the migration. (Optional) + */ + public Integer getChecksum() { + return checksum; + } + + /** + * @return The timestamp when this migration was installed. + */ + public Date getInstalledOn() { + return installedOn; + } + + /** + * @return The user that installed this migration. + */ + public String getInstalledBy() { + return installedBy; + } + + /** + * @return The execution time (in millis) of this migration. + */ + public int getExecutionTime() { + return executionTime; + } + + /** + * @return Flag indicating whether the migration was successful or not. + */ + public boolean isSuccess() { + return success; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AppliedMigration that = (AppliedMigration) o; + + if (executionTime != that.executionTime) return false; + if (installedRank != that.installedRank) return false; + if (success != that.success) return false; + if (checksum != null ? !checksum.equals(that.checksum) : that.checksum != null) return false; + if (!description.equals(that.description)) return false; + if (installedBy != null ? !installedBy.equals(that.installedBy) : that.installedBy != null) return false; + if (installedOn != null ? !installedOn.equals(that.installedOn) : that.installedOn != null) return false; + if (!script.equals(that.script)) return false; + if (type != that.type) return false; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = installedRank; + result = 31 * result + (version != null ? version.hashCode() : 0); + result = 31 * result + description.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + script.hashCode(); + result = 31 * result + (checksum != null ? checksum.hashCode() : 0); + result = 31 * result + (installedOn != null ? installedOn.hashCode() : 0); + result = 31 * result + (installedBy != null ? installedBy.hashCode() : 0); + result = 31 * result + executionTime; + result = 31 * result + (success ? 1 : 0); + return result; + } + + @SuppressWarnings("NullableProblems") + public int compareTo(AppliedMigration o) { + return installedRank - o.installedRank; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/JdbcTableSchemaHistory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/JdbcTableSchemaHistory.java new file mode 100644 index 00000000..520a5a5d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/JdbcTableSchemaHistory.java @@ -0,0 +1,289 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.schemahistory; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.RowMapper; +import org.flywaydb.core.internal.jdbc.ExecutionTemplateFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Supports reading and writing to the schema history table. + */ +class JdbcTableSchemaHistory extends SchemaHistory { + private static final Log LOG = LogFactory.getLog(JdbcTableSchemaHistory.class); + + private final SqlScriptExecutorFactory sqlScriptExecutorFactory; + private final SqlScriptFactory sqlScriptFactory; + + /** + * The database to use. + */ + private final Database database; + + /** + * Connection with access to the database. + */ + private final Connection connection; + + private final JdbcTemplate jdbcTemplate; + + /** + * Applied migration cache. + */ + private final LinkedList cache = new LinkedList<>(); + + /** + * Creates a new instance of the schema history table support. + * + * @param database The database to use. + * @param table The schema history table used by Flyway. + */ + JdbcTableSchemaHistory(SqlScriptExecutorFactory sqlScriptExecutorFactory, SqlScriptFactory sqlScriptFactory, + Database database, Table table) { + this.sqlScriptExecutorFactory = sqlScriptExecutorFactory; + this.sqlScriptFactory = sqlScriptFactory; + this.table = table; + this.database = database; + this.connection = database.getMainConnection(); + this.jdbcTemplate = connection.getJdbcTemplate(); + } + + @Override + public void clearCache() { + cache.clear(); + } + + @Override + public boolean exists() { + connection.restoreOriginalState(); + + return table.exists(); + } + + @Override + public void create(final boolean baseline) { + connection.lock(table, new Callable() { + @Override + public Object call() { + int retries = 0; + while (!exists()) { + if (retries == 0) { + LOG.info("Creating Schema History table " + table + (baseline ? " with baseline" : "") + " ..."); + } + try { + ExecutionTemplateFactory.createExecutionTemplate(connection.getJdbcConnection(), + database).execute(new Callable() { + @Override + public Object call() { + sqlScriptExecutorFactory.createSqlScriptExecutor(connection.getJdbcConnection() + + + + ).execute(database.getCreateScript(sqlScriptFactory, table, baseline)); + LOG.debug("Created Schema History table " + table + (baseline ? " with baseline" : "")); + return null; + } + }); + } catch (FlywayException e) { + if (++retries >= 10) { + throw e; + } + try { + LOG.debug("Schema History table creation failed. Retrying in 1 sec ..."); + Thread.sleep(1000); + } catch (InterruptedException e1) { + // Ignore + } + } + } + return null; + } + }); + } + + @Override + public T lock(Callable callable) { + connection.restoreOriginalState(); + + return connection.lock(table, callable); + } + + @Override + protected void doAddAppliedMigration(int installedRank, MigrationVersion version, String description, + MigrationType type, String script, Integer checksum, + int executionTime, boolean success) { + boolean tableIsLocked = false; + connection.restoreOriginalState(); + + // Lock again for databases with no clean DDL transactions like Oracle + // to prevent implicit commits from triggering deadlocks + // in highly concurrent environments + if (!database.supportsDdlTransactions()) { + table.lock(); + tableIsLocked = true; + } + + try { + String versionStr = version == null ? null : version.toString(); + + if (!database.supportsEmptyMigrationDescription() && "".equals(description)) { + description = NO_DESCRIPTION_MARKER; + } + + jdbcTemplate.update(database.getInsertStatement(table), + installedRank, versionStr, description, type.name(), script, checksum, database.getInstalledBy(), + executionTime, success); + + LOG.debug("Schema History table " + table + " successfully updated to reflect changes"); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to insert row for version '" + version + "' in Schema History table " + table, e); + } finally { + if (tableIsLocked) { + table.unlock(); + } + } + } + + @Override + public List allAppliedMigrations() { + if (!exists()) { + return new ArrayList<>(); + } + + refreshCache(); + return cache; + } + + private void refreshCache() { + int maxCachedInstalledRank = cache.isEmpty() ? -1 : cache.getLast().getInstalledRank(); + + String query = database.getSelectStatement(table); + + try { + cache.addAll(jdbcTemplate.query(query, new RowMapper() { + public AppliedMigration mapRow(final ResultSet rs) throws SQLException { + Integer checksum = rs.getInt("checksum"); + if (rs.wasNull()) { + checksum = null; + } + + // Convert legacy types to their modern equivalent to avoid validation errors + String type = rs.getString("type"); + if ("SPRING_JDBC".equals(type)) { + type = "JDBC"; + } + if ("UNDO_SPRING_JDBC".equals(type)) { + type = "UNDO_JDBC"; + } + + return new AppliedMigration( + rs.getInt("installed_rank"), + rs.getString("version") != null ? MigrationVersion.fromVersion(rs.getString("version")) : null, + rs.getString("description"), + MigrationType.valueOf(type), + rs.getString("script"), + checksum, + rs.getTimestamp("installed_on"), + rs.getString("installed_by"), + rs.getInt("execution_time"), + rs.getBoolean("success") + ); + } + }, maxCachedInstalledRank)); + } catch (SQLException e) { + throw new FlywaySqlException("Error while retrieving the list of applied migrations from Schema History table " + + table, e); + } + } + + @Override + public void removeFailedMigrations() { + if (!exists()) { + LOG.info("Repair of failed migration in Schema History table " + table + " not necessary as table doesn't exist."); + return; + } + + boolean failed = false; + List appliedMigrations = allAppliedMigrations(); + for (AppliedMigration appliedMigration : appliedMigrations) { + if (!appliedMigration.isSuccess()) { + failed = true; + } + } + if (!failed) { + LOG.info("Repair of failed migration in Schema History table " + table + " not necessary. No failed migration detected."); + return; + } + + try { + clearCache(); + jdbcTemplate.execute("DELETE FROM " + table + + " WHERE " + database.quote("success") + " = " + database.getBooleanFalse()); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to repair Schema History table " + table, e); + } + } + + @Override + public void update(AppliedMigration appliedMigration, ResolvedMigration resolvedMigration) { + connection.restoreOriginalState(); + + clearCache(); + + MigrationVersion version = appliedMigration.getVersion(); + + String description = resolvedMigration.getDescription(); + Integer checksum = resolvedMigration.getChecksum(); + MigrationType type = appliedMigration.getType().isSynthetic() + ? appliedMigration.getType() + : resolvedMigration.getType(); + + LOG.info("Repairing Schema History table for version " + version + + " (Description: " + description + ", Type: " + type + ", Checksum: " + checksum + ") ..."); + + try { + jdbcTemplate.update("UPDATE " + table + + " SET " + + database.quote("description") + "=? , " + + database.quote("type") + "=? , " + + database.quote("checksum") + "=?" + + " WHERE " + database.quote("installed_rank") + "=?", + description, type, checksum, appliedMigration.getInstalledRank()); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to repair Schema History table " + table + + " for version " + version, e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistory.java new file mode 100644 index 00000000..4668a25f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistory.java @@ -0,0 +1,195 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.schemahistory; + +import org.flywaydb.core.api.MigrationType; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.resolver.ResolvedMigration; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.util.AbbreviationUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.util.List; +import java.util.concurrent.Callable; + +/** + * The schema history used to track all applied migrations. + */ +public abstract class SchemaHistory { + public static final String NO_DESCRIPTION_MARKER = "<< no description >>"; + + /** + * The schema history table used by Flyway. + * Non-final due to the table name fallback mechanism. Will be made final in Flyway 6.0. + */ + protected Table table; + + /** + * Acquires an exclusive read-write lock on the schema history table. This lock will be released automatically upon completion. + * + * @return The result of the action. + */ + public abstract T lock(Callable callable); + + /** + * @return Whether the schema history table exists. + */ + public abstract boolean exists(); + + /** + * Creates the schema history. Do nothing if it already exists. + * + * @param baseline Whether to include the creation of a baseline marker. + */ + public abstract void create(boolean baseline); + + /** + * Checks whether the schema history table contains at least one non-synthetic applied migration. + * + * @return {@code true} if it does, {@code false} if it doesn't. + */ + public final boolean hasNonSyntheticAppliedMigrations() { + for (AppliedMigration appliedMigration : allAppliedMigrations()) { + if (!appliedMigration.getType().isSynthetic() + + + + ) { + return true; + } + } + return false; + } + + /** + * @return The list of all migrations applied on the schema in the order they were applied (oldest first). + * An empty list if no migration has been applied so far. + */ + public abstract List allAppliedMigrations(); + + /** + * Retrieves the baseline marker from the schema history table. + * + * @return The baseline marker or {@code null} if none could be found. + */ + public final AppliedMigration getBaselineMarker() { + List appliedMigrations = allAppliedMigrations(); + // BASELINE can only be the first or second (in case there is a SCHEMA one) migration. + for (int i = 0; i < Math.min(appliedMigrations.size(), 2); i++) { + AppliedMigration appliedMigration = appliedMigrations.get(i); + if (appliedMigration.getType() == MigrationType.BASELINE) { + return appliedMigration; + } + } + return null; + } + + /** + *

+ * Repairs the schema history table after a failed migration. + * This is only necessary for databases without DDL-transaction support. + *

+ *

+ * On databases with DDL transaction support, a migration failure automatically triggers a rollback of all changes, + * including the ones in the schema history table. + *

+ */ + public abstract void removeFailedMigrations(); + + /** + * Indicates in the schema history table that Flyway created these schemas. + * + * @param schemas The schemas that were created by Flyway. + */ + public final void addSchemasMarker(Schema[] schemas) { + addAppliedMigration(null, "<< Flyway Schema Creation >>", + MigrationType.SCHEMA, StringUtils.arrayToCommaDelimitedString(schemas), null, 0, true); + } + + /** + * Checks whether the schema history table contains a marker row for schema creation. + * + * @return {@code true} if it does, {@code false} if it doesn't. + */ + public final boolean hasSchemasMarker() { + List appliedMigrations = allAppliedMigrations(); + return !appliedMigrations.isEmpty() && appliedMigrations.get(0).getType() == MigrationType.SCHEMA; + } + + + /** + * Updates this applied migration to match this resolved migration. + * + * @param appliedMigration The applied migration to update. + * @param resolvedMigration The resolved migration to source the new values from. + */ + public abstract void update(AppliedMigration appliedMigration, ResolvedMigration resolvedMigration); + + /** + * Clears the applied migration cache. + */ + public void clearCache() { + // Do nothing by default. + } + + /** + * Records a new applied migration. + * + * @param version The target version of this migration. + * @param description The description of the migration. + * @param type The type of migration (BASELINE, SQL, ...) + * @param script The name of the script to execute for this migration, relative to its classpath location. + * @param checksum The checksum of the migration. (Optional) + * @param executionTime The execution time (in millis) of this migration. + * @param success Flag indicating whether the migration was successful or not. + */ + public final void addAppliedMigration(MigrationVersion version, String description, MigrationType type, + String script, Integer checksum, int executionTime, boolean success) { + int installedRank = type == MigrationType.SCHEMA ? 0 : calculateInstalledRank(); + doAddAppliedMigration( + installedRank, + version, + AbbreviationUtils.abbreviateDescription(description), + type, + AbbreviationUtils.abbreviateScript(script), + checksum, + executionTime, + success); + } + + /** + * Calculates the installed rank for the new migration to be inserted. + * + * @return The installed rank. + */ + private int calculateInstalledRank() { + List appliedMigrations = allAppliedMigrations(); + if (appliedMigrations.isEmpty()) { + return 1; + } + return appliedMigrations.get(appliedMigrations.size() - 1).getInstalledRank() + 1; + } + + protected abstract void doAddAppliedMigration(int installedRank, MigrationVersion version, String description, + MigrationType type, String script, Integer checksum, + int executionTime, boolean success); + + @Override + public String toString() { + return table.toString(); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistoryFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistoryFactory.java new file mode 100644 index 00000000..343b83e4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistoryFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.schemahistory; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory; +import org.flywaydb.core.internal.sqlscript.SqlScriptFactory; + +/** + * Factory to obtain a reference to the schema history. + */ +public class SchemaHistoryFactory { + private SchemaHistoryFactory() { + // Prevent instantiation + } + + /** + * Obtains a reference to the schema history. + * + * @param configuration The current Flyway configuration. + * @param database The Database object. + * @param schema The schema whose history to track. + * @return The schema history. + */ + public static SchemaHistory getSchemaHistory(Configuration configuration, + SqlScriptExecutorFactory sqlScriptExecutorFactory, + SqlScriptFactory sqlScriptFactory, + Database database, Schema schema + + + + ) { + Table table = schema.getTable(configuration.getTable()); + JdbcTableSchemaHistory jdbcTableSchemaHistory = + new JdbcTableSchemaHistory(sqlScriptExecutorFactory, sqlScriptFactory, database, table); + + + + + + + + + + return jdbcTableSchemaHistory; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/package-info.java new file mode 100644 index 00000000..2a1638d8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.schemahistory; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java new file mode 100644 index 00000000..f3b6345e --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java @@ -0,0 +1,298 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.api.callback.Error; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.callback.Warning; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.Result; +import org.flywaydb.core.internal.jdbc.Results; +import org.flywaydb.core.internal.util.AsciiTable; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DefaultSqlScriptExecutor implements SqlScriptExecutor { + private static final Log LOG = LogFactory.getLog(DefaultSqlScriptExecutor.class); + + private final JdbcTemplate jdbcTemplate; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public DefaultSqlScriptExecutor(JdbcTemplate jdbcTemplate + + + + + ) { + this.jdbcTemplate = jdbcTemplate; + + + + + + + + } + + @Override + public void execute(SqlScript sqlScript) { + + + + + + + + + try (SqlStatementIterator sqlStatementIterator = sqlScript.getSqlStatements()) { + while (sqlStatementIterator.hasNext()) { + SqlStatement sqlStatement = sqlStatementIterator.next(); + + + + + + + + + + + + + + + + + + + + + + + + + + + + executeStatement(jdbcTemplate, sqlScript, sqlStatement); + + + + } + } + + + + + + + + } + + protected void logStatementExecution(SqlStatement sqlStatement) { + if (LOG.isDebugEnabled()) { + LOG.debug("Executing " + + + + + "SQL: " + sqlStatement.getSql()); + } + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + protected void executeStatement(JdbcTemplate jdbcTemplate, SqlScript sqlScript, SqlStatement sqlStatement) { + logStatementExecution(sqlStatement); + String sql = sqlStatement.getSql() + sqlStatement.getDelimiter(); + + + + + + + Results results = sqlStatement.execute(jdbcTemplate + + + + ); + if (results.getException() != null) { + + + + + + printWarnings(results); + handleException(results, sqlScript, sqlStatement); + return; + } + + + + + + + printWarnings(results); + handleResults(results + + + + ); + } + + protected void handleResults(Results results + + + + ) { + for (Result result : results.getResults()) { + long updateCount = result.getUpdateCount(); + if (updateCount != -1) { + handleUpdateCount(updateCount); + } + + outputQueryResult(result); + + } + } + + protected void outputQueryResult(Result result) { + if ( + + + + result.getColumns() != null) { + LOG.info(new AsciiTable(result.getColumns(), result.getData(), + true, "", "No rows returned").render()); + } + } + + private void handleUpdateCount(long updateCount) { + if (LOG.isDebugEnabled()) { + LOG.debug("Update Count: " + updateCount); + } + } + + protected void handleException(Results results, SqlScript sqlScript, SqlStatement sqlStatement) { + + + + + throw new FlywaySqlScriptException(sqlScript.getResource(), sqlStatement, results.getException()); + + + + + } + + private void printWarnings(Results results) { + for (Warning warning : results.getWarnings()) { + + + + if ("00000".equals(warning.getState())) { + LOG.info("DB: " + warning.getMessage()); + } else { + LOG.warn("DB: " + warning.getMessage() + + " (SQL State: " + warning.getState() + " - Error Code: " + warning.getCode() + ")"); + } + + + + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/Delimiter.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/Delimiter.java new file mode 100644 index 00000000..3d242b8c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/Delimiter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +/** + * Represents a sql statement delimiter. + */ +public class Delimiter { + public static final Delimiter SEMICOLON = new Delimiter(";", false + + + + ); + public static final Delimiter GO = new Delimiter("GO", true + + + + ); + + /** + * The actual delimiter string. + */ + private final String delimiter; + + /** + * Whether the delimiter sits alone on a new line or not. + */ + private final boolean aloneOnLine; + + + + + + + + + /** + * Creates a new delimiter. + * + * @param delimiter The actual delimiter string. + * @param aloneOnLine Whether the delimiter sits alone on a new line or not. + */ + public Delimiter(String delimiter, boolean aloneOnLine + + + + ) { + this.delimiter = delimiter; + this.aloneOnLine = aloneOnLine; + + + + } + + /** + * @return The actual delimiter string. + */ + public String getDelimiter() { + return delimiter; + } + + /** + * @return Whether the delimiter sits alone on a new line or not. + */ + public boolean isAloneOnLine() { + return aloneOnLine; + } + + + + + + + + + + + + @Override + public String toString() { + return (aloneOnLine ? "\n" : "") + delimiter; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Delimiter delimiter1 = (Delimiter) o; + + return aloneOnLine == delimiter1.aloneOnLine && delimiter.equals(delimiter1.delimiter); + } + + @Override + public int hashCode() { + int result = delimiter.hashCode(); + result = 31 * result + (aloneOnLine ? 1 : 0); + return result; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/FlywaySqlScriptException.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/FlywaySqlScriptException.java new file mode 100644 index 00000000..ad2fdbca --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/FlywaySqlScriptException.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.resource.Resource; + +import java.sql.SQLException; + +/** + * This specific exception thrown when Flyway encounters a problem in SQL script + */ +public class FlywaySqlScriptException extends FlywaySqlException { + private final Resource resource; + private final SqlStatement statement; + + /** + * Creates new instance of FlywaySqlScriptException. + * + * @param resource The resource containing the failed statement. + * @param statement The failed SQL statement. + * @param sqlException Cause of the problem. + */ + public FlywaySqlScriptException(Resource resource, SqlStatement statement, SQLException sqlException) { + super(resource == null ? "Script failed" : "Migration " + resource.getFilename() + " failed", sqlException); + this.resource = resource; + this.statement = statement; + } + + /** + * @return The resource containing the failed statement. + */ + public Resource getResource() { + return resource; + } + + /** + * Returns the line number in migration SQL script where exception occurred. + * + * @return The line number. + */ + public int getLineNumber() { + return statement == null ? -1 : statement.getLineNumber(); + } + + /** + * Returns the failed statement in SQL script. + * + * @return The failed statement. + */ + public String getStatement() { + return statement == null ? "" : statement.getSql(); + } + + /** + * Returns the failed statement in SQL script. + * + * @return The failed statement. + */ + public SqlStatement getSqlStatement() { + return statement; + } + + @Override + public String getMessage() { + String message = super.getMessage(); + if (resource != null) { + message += "Location : " + resource.getAbsolutePath() + " (" + resource.getAbsolutePathOnDisk() + ")\n"; + } + if (statement != null) { + message += "Line : " + getLineNumber() + "\n"; + message += "Statement : " + getStatement() + "\n"; + } + return message; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParsedSqlStatement.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParsedSqlStatement.java new file mode 100644 index 00000000..48f6a7a2 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParsedSqlStatement.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.Results; + +/** + * A sql statement from a script that can be executed at once against a database. + */ +public class ParsedSqlStatement implements SqlStatement { + private final int pos; + private final int line; + private final int col; + private final String sql; + + public int getPos() { + return pos; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + /** + * The delimiter of the statement. + */ + private final Delimiter delimiter; + + private final boolean canExecuteInTransaction; + + + + + + + + + public ParsedSqlStatement(int pos, int line, int col, String sql, Delimiter delimiter, + boolean canExecuteInTransaction + + + + ) { + this.pos = pos; + this.line = line; + this.col = col; + this.sql = sql; + this.delimiter = delimiter; + this.canExecuteInTransaction = canExecuteInTransaction; + + + + } + + @Override + public final int getLineNumber() { + return line; + } + + @Override + public final String getSql() { + return sql; + } + + @Override + public String getDelimiter() { + return delimiter.toString(); + } + + @Override + public boolean canExecuteInTransaction() { + return canExecuteInTransaction; + } + + + + + + + + + + + + + + @Override + public Results execute(JdbcTemplate jdbcTemplate + + + + ) { + return jdbcTemplate.executeStatement(sql); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParserSqlScript.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParserSqlScript.java new file mode 100644 index 00000000..a84f3162 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParserSqlScript.java @@ -0,0 +1,205 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.resource.LoadableResource; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +public class ParserSqlScript implements SqlScript { + private static final Log LOG = LogFactory.getLog(ParserSqlScript.class); + + /** + * The sql statements contained in this script. + */ + protected final List sqlStatements = new ArrayList<>(); + + private int sqlStatementCount; + + /** + * Whether this SQL script contains at least one non-transactional statement. + */ + private boolean nonTransactionalStatementFound; + + /** + * The resource containing the statements. + */ + protected final LoadableResource resource; + + private final SqlScriptMetadata metadata; + protected final Parser parser; + private final boolean mixed; + private boolean parsed; + + + + + + + /** + * Creates a new sql script from this source. + * + * @param resource The sql script resource. + * @param metadataResource The sql script metadata resource. + * @param mixed Whether to allow mixing transactional and non-transactional statements within the same migration. + */ + public ParserSqlScript(Parser parser, LoadableResource resource, LoadableResource metadataResource, boolean mixed) { + this.resource = resource; + this.metadata = SqlScriptMetadata.fromResource(metadataResource); + this.parser = parser; + + + + this.mixed = mixed; + } + + protected void parse() { + try (SqlStatementIterator sqlStatementIterator = parser.parse(resource)) { + boolean transactionalStatementFound = false; + while (sqlStatementIterator.hasNext()) { + SqlStatement sqlStatement = sqlStatementIterator.next(); + + + + this.sqlStatements.add(sqlStatement); + + + + + sqlStatementCount++; + + if (sqlStatement.canExecuteInTransaction()) { + transactionalStatementFound = true; + } else { + nonTransactionalStatementFound = true; + } + + if (!mixed && transactionalStatementFound && nonTransactionalStatementFound && metadata.executeInTransaction() == null) { + throw new FlywayException( + "Detected both transactional and non-transactional statements within the same migration" + + " (even though mixed is false). Offending statement found at line " + + sqlStatement.getLineNumber() + ": " + sqlStatement.getSql() + + (sqlStatement.canExecuteInTransaction() ? "" : " [non-transactional]")); + } + + + + + + + + + + if (LOG.isDebugEnabled()) { + LOG.debug("Found statement at line " + sqlStatement.getLineNumber() + ": " + sqlStatement.getSql() + + (sqlStatement.canExecuteInTransaction() ? "" : " [non-transactional]")); + } + } + } + parsed = true; + } + + @Override + public void validate() { + if (!parsed) { + parse(); + } + } + + @Override + public SqlStatementIterator getSqlStatements() { + validate(); + + + + + + + + final Iterator iterator = sqlStatements.iterator(); + return new SqlStatementIterator() { + @Override + public void close() { + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public SqlStatement next() { + return iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } + + @Override + public int getSqlStatementCount() { + validate(); + + return sqlStatementCount; + } + + + + + + + + + + + + + + @Override + public final LoadableResource getResource() { + return resource; + } + + @Override + public boolean executeInTransaction() { + Boolean executeInTransactionOverride = metadata.executeInTransaction(); + if (executeInTransactionOverride != null) { + LOG.debug("Using executeInTransaction=" + executeInTransactionOverride + " from script configuration"); + return executeInTransactionOverride; + } + + validate(); + + return !nonTransactionalStatementFound; + } + + @Override + public int compareTo(SqlScript o) { + return resource.getRelativePath().compareTo(o.getResource().getRelativePath()); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScript.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScript.java new file mode 100644 index 00000000..1d437509 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScript.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.resource.LoadableResource; + +import java.util.Collection; + +/** + * SQL script containing a series of statements terminated by a delimiter (eg: ;). + * Single-line (--) and multi-line (/* * /) comments are stripped and ignored. + */ +public interface SqlScript extends Comparable { + /** + * @return The sql statements contained in this script. + */ + SqlStatementIterator getSqlStatements(); + + /** + * @return The number of sql statements contained in this script. + */ + int getSqlStatementCount(); + + + + + + + + + + /** + * @return The resource containing the statements. + */ + LoadableResource getResource(); + + /** + * Whether the execution should take place inside a transaction. This is useful for databases + * like PostgreSQL where certain statement can only execute outside a transaction. + * + * @return {@code true} if a transaction should be used (highly recommended), or {@code false} if not. + */ + boolean executeInTransaction(); + + /** + * Validates this SQL script. + */ + void validate(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutor.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutor.java new file mode 100644 index 00000000..421cb15a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +/** + * Executor for SQL scripts. + */ +public interface SqlScriptExecutor { + /** + * Executes this SQL script. + * + * @param sqlScript The SQL script. + */ + void execute(SqlScript sqlScript); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutorFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutorFactory.java new file mode 100644 index 00000000..54b4c61d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutorFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import java.sql.Connection; + +public interface SqlScriptExecutorFactory { + /** + * Creates a new executor for this SQL script. + * + * @return A new SQL script executor. + */ + SqlScriptExecutor createSqlScriptExecutor(Connection connection + + + + ); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptFactory.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptFactory.java new file mode 100644 index 00000000..777925bf --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceProvider; + +public interface SqlScriptFactory { + /** + * @return A new SQL script. + */ + SqlScript createSqlScript(LoadableResource resource, boolean mixed, ResourceProvider resourceProvider); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.java new file mode 100644 index 00000000..cbad912f --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.ResourceProvider; + +import java.util.HashMap; +import java.util.Map; + +import static org.flywaydb.core.internal.configuration.ConfigUtils.removeBoolean; + +public class SqlScriptMetadata { + private static final Log LOG = LogFactory.getLog(SqlScriptMetadata.class); + private static final String EXECUTE_IN_TRANSACTION = "executeInTransaction"; + + private final Boolean executeInTransaction; + + private SqlScriptMetadata(Map metadata) { + // Make copy to prevent removing elements from the original + metadata = new HashMap<>(metadata); + this.executeInTransaction = removeBoolean(metadata, EXECUTE_IN_TRANSACTION); + + ConfigUtils.checkConfigurationForUnrecognisedProperties(metadata, null); + } + + public Boolean executeInTransaction() { + return executeInTransaction; + } + + public static SqlScriptMetadata fromResource(LoadableResource resource) { + if (resource != null) { + LOG.debug("Found script configuration: " + resource.getFilename()); + return new SqlScriptMetadata(ConfigUtils.loadConfigurationFromReader(resource.read())); + } + return new SqlScriptMetadata(new HashMap<>()); + } + + public static LoadableResource getMetadataResource(ResourceProvider resourceProvider, LoadableResource resource) { + if (resourceProvider == null) { + return null; + } + return resourceProvider.getResource(resource.getRelativePath() + ".conf"); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatement.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatement.java new file mode 100644 index 00000000..9702f42c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatement.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.jdbc.Results; + +/** + * A sql statement from a script that can be executed at once against a database. + */ +public interface SqlStatement { + /** + * @return The original line number where the statement was located in the script it came from. + */ + int getLineNumber(); + + /** + * @return The sql to send to the database. + */ + String getSql(); + + /** + * @return The delimiter for the statement. + */ + String getDelimiter(); + + /** + * Whether the execution should take place inside a transaction. Almost all implementation should return {@code true}. + * This however makes it possible to execute certain migrations outside a transaction. This is useful for databases + * like PostgreSQL and SQL Server where certain statement can only execute outside a transaction. + * + * @return {@code true} if a transaction should be used (highly recommended), or {@code false} if not. + */ + boolean canExecuteInTransaction(); + + + + + + + + + + + + + + + /** + * Executes this statement against the database. + * + * @param jdbcTemplate The jdbcTemplate to use to execute this script. + * @return the result of the execution. + */ + Results execute(JdbcTemplate jdbcTemplate + + + + ); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatementIterator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatementIterator.java new file mode 100644 index 00000000..b90614ae --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatementIterator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.sqlscript; + +import org.flywaydb.core.internal.sqlscript.SqlStatement; +import org.flywaydb.core.internal.util.CloseableIterator; + +public interface SqlStatementIterator extends CloseableIterator { + @Override + void close(); +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/package-info.java new file mode 100644 index 00000000..9675b7fe --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.sqlscript; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/AbbreviationUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AbbreviationUtils.java new file mode 100644 index 00000000..caaaea24 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AbbreviationUtils.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +/** + * Various abbreviation-related utilities. + */ +public class AbbreviationUtils { + /** + * Prevents instantiation. + */ + private AbbreviationUtils() { + // Do nothing. + } + + /** + * Abbreviates this description to a length that will fit in the database. + * + * @param description The description to process. + * @return The abbreviated version. + */ + public static String abbreviateDescription(String description) { + if (description == null) { + return null; + } + + if (description.length() <= 200) { + return description; + } + + return description.substring(0, 197) + "..."; + } + + /** + * Abbreviates this script to a length that will fit in the database. + * + * @param script The script to process. + * @return The abbreviated version. + */ + public static String abbreviateScript(String script) { + if (script == null) { + return null; + } + + if (script.length() <= 1000) { + return script; + } + + return "..." + script.substring(3, 1000); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/AsciiTable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AsciiTable.java new file mode 100644 index 00000000..9fb49768 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AsciiTable.java @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * An Ascii table. + */ +public class AsciiTable { + private static final String DEFAULT_COLUMN_NAME = "(No column name)"; + + private final List columns; + private final List> rows; + private final boolean printHeader; + private final String nullText; + private final String emptyText; + + /** + * Creates a new Ascii table. + * + * @param columns The column titles. + * @param rows The data rows + * @param printHeader Whether to print the header row or not. + * @param nullText The text to use for a {@code null} value. + * @param emptyText The text to include in the table if it has no rows. + */ + public AsciiTable(List columns, List> rows, boolean printHeader, String nullText, String emptyText) { + this.columns = ensureValidColumns(columns); + this.rows = rows; + this.printHeader = printHeader; + this.nullText = nullText; + this.emptyText = emptyText; + } + + private static List ensureValidColumns(List columns) { + List validColumns = new ArrayList<>(); + for (String column : columns) { + validColumns.add(column != null ? column : DEFAULT_COLUMN_NAME); + } + return validColumns; + } + + /** + * @return The table rendered with column header and row data. + */ + public String render() { + List widths = new ArrayList<>(); + for (String column : columns) { + widths.add(column.length()); + } + + for (List row : rows) { + for (int i = 0; i < row.size(); i++) { + widths.set(i, Math.max(widths.get(i), getValue(row, i).length())); + } + } + + StringBuilder ruler = new StringBuilder("+"); + for (Integer width : widths) { + ruler.append("-").append(StringUtils.trimOrPad("", width, '-')).append("-+"); + } + ruler.append("\n"); + + StringBuilder result = new StringBuilder(); + + if (printHeader) { + StringBuilder header = new StringBuilder("|"); + for (int i = 0; i < widths.size(); i++) { + header.append(" ").append(StringUtils.trimOrPad(columns.get(i), widths.get(i), ' ')).append(" |"); + } + header.append("\n"); + + result.append(ruler); + result.append(header); + } + + result.append(ruler); + + if (rows.isEmpty()) { + result.append("| ").append(StringUtils.trimOrPad(emptyText, ruler.length() - 5)).append(" |\n"); + } else { + for (List row : rows) { + StringBuilder r = new StringBuilder("|"); + for (int i = 0; i < widths.size(); i++) { + r.append(" ").append(StringUtils.trimOrPad(getValue(row, i), widths.get(i), ' ')).append(" |"); + } + r.append("\n"); + result.append(r); + } + } + + result.append(ruler); + return result.toString(); + } + + private String getValue(List row, int i) { + String value = row.get(i); + if (value == null) { + value = nullText; + } + return value; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomFilter.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomFilter.java new file mode 100644 index 00000000..3060cce1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +public class BomFilter { + private static final char BOM = '\ufeff'; + + /** + * Determine if this char is a UTF-8 Byte Order Mark + * @param c The char to check + * @return Whether this char is a UTF-8 Byte Order Mark + */ + public static boolean isBom(char c) { + return c == BOM; + } + + /** + * Removes the UTF-8 Byte Order Mark from the start of a string if present. + * @param s The string + * @return The string without a Byte Order Mark at the start + */ + public static String FilterBomFromString(String s) { + if (s.isEmpty()) { + return s; + } + + if (isBom(s.charAt(0))) { + return s.substring(1); + } + + return s; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomStrippingReader.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomStrippingReader.java new file mode 100644 index 00000000..ac922930 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomStrippingReader.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +/** + * Reader that strips the BOM at the beginning of a stream. + */ +public class BomStrippingReader extends FilterReader { + private static final int EMPTY_STREAM = -1; + + /** + * Creates a new BOM-stripping reader. + * + * @param in a Reader object providing the underlying stream. + * @throws NullPointerException if in is null + */ + public BomStrippingReader(Reader in) { + super(in); + } + + @Override + public int read() throws IOException { + int c = super.read(); + if (c != EMPTY_STREAM && BomFilter.isBom((char)c)) { + // Skip BOM + return super.read(); + } + return c; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/ClassUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ClassUtils.java new file mode 100644 index 00000000..890cdfd4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ClassUtils.java @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods for dealing with classes. + */ +public class ClassUtils { + private static final Log LOG = LogFactory.getLog(ClassUtils.class); + + /** + * Prevents instantiation. + */ + private ClassUtils() { + // Do nothing + } + + /** + * Creates a new instance of this class. + * + * @param className The fully qualified name of the class to instantiate. + * @param classLoader The ClassLoader to use. + * @param The type of the new instance. + * @return The new instance. + * @throws FlywayException Thrown when the instantiation failed. + */ + @SuppressWarnings({"unchecked"}) + // Must be synchronized for the Maven Parallel Junit runner to work + public static synchronized T instantiate(String className, ClassLoader classLoader) { + try { + return (T) Class.forName(className, true, classLoader).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new FlywayException("Unable to instantiate class " + className + " : " + e.getMessage(), e); + } + } + + /** + * Creates a new instance of this class. + * + * @param clazz The class to instantiate. + * @param The type of the new instance. + * @return The new instance. + * @throws FlywayException Thrown when the instantiation failed. + */ + // Must be synchronized for the Maven Parallel Junit runner to work + public static synchronized T instantiate(Class clazz) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new FlywayException("Unable to instantiate class " + clazz.getName() + " : " + e.getMessage(), e); + } + } + + /** + * Instantiate all these classes. + * + * @param classes A fully qualified class names to instantiate. + * @param classLoader The ClassLoader to use. + * @param The common type for all classes. + * @return The list of instances. + */ + public static List instantiateAll(String[] classes, ClassLoader classLoader) { + List clazzes = new ArrayList<>(); + for (String clazz : classes) { + if (StringUtils.hasLength(clazz)) { + clazzes.add(ClassUtils.instantiate(clazz, classLoader)); + } + } + return clazzes; + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return {@code false} if either the class or + * one of its dependencies is not present or cannot be loaded. + * + * @param className The name of the class to check. + * @param classLoader The ClassLoader to use. + * @return whether the specified class is present + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + try { + classLoader.loadClass(className); + return true; + } catch (Throwable ex) { + // Class or one of its dependencies is not present... + return false; + } + } + + /** + * Loads the class with this name using the class loader. + * + * @param implementedInterface The interface the class is expected to implement. + * @param className The name of the class to load. + * @param classLoader The ClassLoader to use. + * @return the newly loaded class or {@code null} if it could not be loaded. + */ + public static Class loadClass(Class implementedInterface, String className, ClassLoader classLoader) { + try { + Class clazz = classLoader.loadClass(className); + + if (!implementedInterface.isAssignableFrom(clazz)) { + return null; + } + + if (Modifier.isAbstract(clazz.getModifiers()) || clazz.isEnum() || clazz.isAnonymousClass()) { + LOG.debug("Skipping non-instantiable class: " + className); + return null; + } + + clazz.getDeclaredConstructor().newInstance(); + LOG.debug("Found class: " + className); + //noinspection unchecked + return (Class) clazz; + } catch (Throwable e) { + Throwable rootCause = ExceptionUtils.getRootCause(e); + LOG.warn("Skipping " + className + ": " + formatThrowable(e) + ( + rootCause == e + ? "" + : " caused by " + formatThrowable(rootCause) + + " at " + ExceptionUtils.getThrowLocation(rootCause) + )); + return null; + } + } + + private static String formatThrowable(Throwable e) { + return "(" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"; + } + + /** + * Retrieves the physical location on disk of this class. + * + * @param aClass The class to get the location for. + * @return The absolute path of the .class file. + */ + public static String getLocationOnDisk(Class aClass) { + ProtectionDomain protectionDomain = aClass.getProtectionDomain(); + if (protectionDomain == null) { + //Android + return null; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + //Custom classloader with for example classes defined using URLClassLoader#defineClass(String name, byte[] b, int off, int len) + return null; + } + return UrlUtils.decodeURL(codeSource.getLocation().getPath()); + } + + /** + * Adds these jars or directories to the classpath. + * + * @param classLoader The current ClassLoader. + * @param jarFiles The jars or directories to add. + * @return The new ClassLoader containing the additional jar or directory. + */ + public static ClassLoader addJarsOrDirectoriesToClasspath(ClassLoader classLoader, List jarFiles) { + List urls = new ArrayList<>(); + for (File jarFile : jarFiles) { + LOG.debug("Adding location to classpath: " + jarFile.getAbsolutePath()); + + try { + urls.add(jarFile.toURI().toURL()); + } catch (Exception e) { + throw new FlywayException("Unable to load " + jarFile.getPath(), e); + } + } + return new URLClassLoader(urls.toArray(new URL[0]), classLoader); + } + + + /** + * Gets the String value of a static field. + * + * @param className The fully qualified name of the class to instantiate. + * @param classLoader The ClassLoader to use. + * @param fieldName The field name + * @return The value of the field. + * @throws FlywayException Thrown when the instantiation failed. + */ + public static String getStaticFieldValue(String className, String fieldName, ClassLoader classLoader) { + try { + Class clazz = Class.forName(className, true, classLoader); + Field field = clazz.getField(fieldName); + return (String)field.get(null); + } catch (Exception e) { + throw new FlywayException("Unable to obtain field value " + className + "." + fieldName + " : " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/CloseableIterator.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/CloseableIterator.java new file mode 100644 index 00000000..85b6ed70 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/CloseableIterator.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.util.Iterator; + +/** + * Iterator that can be used to close underlying resources. + * + * @param The typo of element to iterate on. + */ +public interface CloseableIterator extends Iterator, AutoCloseable { +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/DateUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/DateUtils.java new file mode 100644 index 00000000..907691b1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/DateUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +/** + * Utility methods for dealing with dates. + */ +public class DateUtils { + /** + * Prevents instantiation. + */ + private DateUtils() { + // Do nothing + } + + /** + * Formats this date in the standard ISO yyyy-MM-dd HH:mm:ss format. + * + * @param date The date to format. + * @return The date in ISO format. An empty string if the date is null. + */ + public static String formatDateAsIsoString(Date date) { + if (date == null) { + return ""; + } + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + } + + /** + * Formats the time of this date in the standard ISO HH:mm:ss format. + * + * @param date The date to format. + * @return The time in ISO format. An empty string if the time is null. + */ + public static String formatTimeAsIsoString(Date date) { + if (date == null) { + return ""; + } + return new SimpleDateFormat("HH:mm:ss").format(date); + } + + /** + * Create a new date with this year, month and day. + * + * @param year The year. + * @param month The month (1-12). + * @param day The day (1-31). + * @return The date. + */ + public static Date toDate(int year, int month, int day) { + return new GregorianCalendar(year, month - 1, day).getTime(); + } + + /** + * Converts this date into a YYYY-MM-dd string. + * + * @param date The date. + * @return The matching string. + */ + public static String toDateString(Date date) { + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(date); + String year = "" + calendar.get(Calendar.YEAR); + String month = StringUtils.trimOrLeftPad("" + (calendar.get(Calendar.MONTH) + 1), 2, '0'); + String day = StringUtils.trimOrLeftPad("" + calendar.get(Calendar.DAY_OF_MONTH), 2, '0'); + return year + "-" + month + "-" + day; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/ExceptionUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ExceptionUtils.java new file mode 100644 index 00000000..154b6347 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ExceptionUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.sql.SQLException; + +/** + * Utility class for dealing with exceptions. + */ +public class ExceptionUtils { + /** + * Prevents instantiation. + */ + private ExceptionUtils() { + //Do nothing + } + + /** + * Returns the root cause of this throwable. + * + * @param throwable The throwable to inspect. + * @return The root cause or the throwable itself if it doesn't have a cause. + */ + public static Throwable getRootCause(Throwable throwable) { + if (throwable == null) { + return null; + } + + Throwable cause = throwable; + Throwable rootCause; + while ((rootCause = cause.getCause()) != null) { + cause = rootCause; + } + + return cause; + } + + /** + * Retrives the exact location where this exception was thrown. + * + * @param e The exception. + * @return The location, suitable for a debug message. + */ + public static String getThrowLocation(Throwable e) { + StackTraceElement element = e.getStackTrace()[0]; + int lineNumber = element.getLineNumber(); + return element.getClassName() + "." + element.getMethodName() + + (lineNumber < 0 ? "" : ":" + lineNumber) + + (element.isNativeMethod() ? " [native]" : ""); + } + + /** + * Transforms the details of this SQLException into a nice readable message. + * + * @param e The exception. + * @return The message. + */ + public static String toMessage(SQLException e) { + SQLException cause = e; + while (cause.getNextException() != null) { + cause = cause.getNextException(); + } + + String message = "SQL State : " + cause.getSQLState() + "\n" + + "Error Code : " + cause.getErrorCode() + "\n"; + if (cause.getMessage() != null) { + message += "Message : " + cause.getMessage().trim() + "\n"; + } + + return message; + + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/FeatureDetector.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FeatureDetector.java new file mode 100644 index 00000000..33bbc6d8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FeatureDetector.java @@ -0,0 +1,153 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +/** + * Detects whether certain features are available or not. + */ +public final class FeatureDetector { + private static final Log LOG = LogFactory.getLog(FeatureDetector.class); + + /** + * The ClassLoader to use. + */ + private ClassLoader classLoader; + + /** + * Creates a new FeatureDetector. + * + * @param classLoader The ClassLoader to use. + */ + public FeatureDetector(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Flag indicating availability of the Apache Commons Logging. + */ + private Boolean apacheCommonsLoggingAvailable; + + /** + * Flag indicating availability of the Slf4j. + */ + private Boolean slf4jAvailable; + + /** + * Flag indicating availability of JBoss VFS v2. + */ + private Boolean jbossVFSv2Available; + + /** + * Flag indicating availability of JBoss VFS v3. + */ + private Boolean jbossVFSv3Available; + + /** + * Flag indicating availability of the OSGi framework classes. + */ + private Boolean osgiFrameworkAvailable; + + /** + * Flag indicating availability of the Android classes. + */ + private Boolean androidAvailable; + + /** + * Checks whether Apache Commons Logging is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isApacheCommonsLoggingAvailable() { + if (apacheCommonsLoggingAvailable == null) { + apacheCommonsLoggingAvailable = ClassUtils.isPresent("org.apache.commons.logging.Log", classLoader); + } + + return apacheCommonsLoggingAvailable; + } + + /** + * Checks whether Slf4j is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isSlf4jAvailable() { + if (slf4jAvailable == null) { + slf4jAvailable = ClassUtils.isPresent("org.slf4j.Logger", classLoader); + } + + return slf4jAvailable; + } + + /** + * Checks whether JBoss VFS v2 is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isJBossVFSv2Available() { + if (jbossVFSv2Available == null) { + jbossVFSv2Available = ClassUtils.isPresent("org.jboss.virtual.VFS", classLoader); + LOG.debug("JBoss VFS v2 available: " + jbossVFSv2Available); + } + + return jbossVFSv2Available; + } + + /** + * Checks whether JBoss VFS is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isJBossVFSv3Available() { + if (jbossVFSv3Available == null) { + jbossVFSv3Available = ClassUtils.isPresent("org.jboss.vfs.VFS", classLoader); + LOG.debug("JBoss VFS v3 available: " + jbossVFSv3Available); + } + + return jbossVFSv3Available; + } + + /** + * Checks if OSGi framework is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isOsgiFrameworkAvailable() { + if (osgiFrameworkAvailable == null) { + // Use this class' classloader to detect the OSGi framework + ClassLoader classLoader = FeatureDetector.class.getClassLoader(); + osgiFrameworkAvailable = ClassUtils.isPresent("org.osgi.framework.Bundle", classLoader); + LOG.debug("OSGi framework available: " + osgiFrameworkAvailable); + } + + return osgiFrameworkAvailable; + } + + /** + * Checks if Android is available. + * + * @return {@code true} if it is, {@code false if it is not} + */ + public boolean isAndroidAvailable() { + if (androidAvailable == null) { + androidAvailable = "Android Runtime".equals(System.getProperty("java.runtime.name")); + } + + return androidAvailable; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/FileCopyUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FileCopyUtils.java new file mode 100644 index 00000000..fd3caa90 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FileCopyUtils.java @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.Charset; + +/** + * Utility class for copying files and their contents. Inspired by Spring's own. + */ +public class FileCopyUtils { + /** + * Prevent instantiation. + */ + private FileCopyUtils() { + // Do nothing + } + + /** + * Copy the contents of the given Reader into a String. + * Closes the reader when done. + * + * @param in the reader to copy from + * @return the String that has been copied to + * @throws java.io.IOException in case of I/O errors + */ + public static String copyToString(Reader in) throws IOException { + StringWriter out = new StringWriter(); + copy(in, out); + String str = out.toString(); + + //Strip UTF-8 BOM if necessary + if (str.startsWith("\ufeff")) { + return str.substring(1); + } + + return str; + } + + /** + * Copy the contents of the given InputStream into a new byte array. + * Closes the stream when done. + * + * @param in the stream to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(4096); + copy(in, out); + return out.toByteArray(); + } + + /** + * Copy the contents of the given InputStream into a new String based on this encoding. + * Closes the stream when done. + * + * @param in the stream to copy from + * @param encoding The encoding to use. + * @return The new String. + * @throws IOException in case of I/O errors + */ + public static String copyToString(InputStream in, Charset encoding) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(4096); + copy(in, out); + return out.toString(encoding.name()); + } + + /** + * Copy the contents of the given Reader to the given Writer. + * Closes both when done. + * + * @param in the Reader to copy from + * @param out the Writer to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(Reader in, Writer out) throws IOException { + try { + char[] buffer = new char[4096]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } finally { + IOUtils.close(in); + IOUtils.close(out); + } + } + + /** + * Copy the contents of the given InputStream to the given OutputStream. + * Closes both streams when done. + * + * @param in the stream to copy from + * @param out the stream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + try { + int byteCount = 0; + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } finally { + IOUtils.close(in); + IOUtils.close(out); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/IOUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/IOUtils.java new file mode 100644 index 00000000..ded66eb7 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/IOUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +/** + * General IO-related utilities. + */ +public class IOUtils { + private IOUtils() { + } + + /** + * Closes this closeable and never fail while doing so. + * + * @param closeable The closeable to close. Can be {@code null}. + */ + public static void close(AutoCloseable closeable) { + if (closeable == null) { + return; + } + + try { + closeable.close(); + } catch (Exception e) { + // Ignore + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/Locations.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Locations.java new file mode 100644 index 00000000..f343fe5d --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Locations.java @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Encapsulation of a location list. + */ +public class Locations { + private static final Log LOG = LogFactory.getLog(Locations.class); + + /** + * The backing list. + */ + private final List locations = new ArrayList<>(); + + /** + * Creates a new Locations wrapper with these raw locations. + * + * @param rawLocations The raw locations to process. + */ + public Locations(String... rawLocations) { + List normalizedLocations = new ArrayList<>(); + for (String rawLocation : rawLocations) { + normalizedLocations.add(new Location(rawLocation)); + } + processLocations(normalizedLocations); + } + + /** + * Creates a new Locations wrapper with these locations. + * + * @param rawLocations The locations to process. + */ + public Locations(List rawLocations) { + processLocations(rawLocations); + } + + private void processLocations(List rawLocations) { + List sortedLocations = new ArrayList<>(rawLocations); + Collections.sort(sortedLocations); + + for (Location normalizedLocation : sortedLocations) { + if (locations.contains(normalizedLocation)) { + LOG.warn("Discarding duplicate location '" + normalizedLocation + "'"); + continue; + } + + Location parentLocation = getParentLocationIfExists(normalizedLocation, locations); + if (parentLocation != null) { + LOG.warn("Discarding location '" + normalizedLocation + "' as it is a sublocation of '" + parentLocation + "'"); + continue; + } + + locations.add(normalizedLocation); + } + } + + /** + * @return The locations. + */ + public List getLocations() { + return locations; + } + + /** + * Retrieves this location's parent within this list, if any. + * + * @param location The location to check. + * @param finalLocations The list to search. + * @return The parent location. {@code null} if none. + */ + private Location getParentLocationIfExists(Location location, List finalLocations) { + for (Location finalLocation : finalLocations) { + if (finalLocation.isParentOf(location)) { + return finalLocation; + } + } + return null; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/Pair.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Pair.java new file mode 100644 index 00000000..01a40ddc --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Pair.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.util.Arrays; + +/** + * A simple pair of values. + */ +public class Pair implements Comparable> { + /** + * The left side of the pair. + */ + private final L left; + + /** + * The right side of the pair. + */ + private final R right; + + private Pair(L left, R right) { + this.left = left; + this.right = right; + } + + /** + * Creates a new pair of these values. + * + * @param left The left side of the pair. + * @param right The right side of the pair. + * @return The pair. + */ + public static Pair of(L left, R right) { + return new Pair(left, right); + } + + /** + * @return The left side of the pair. + */ + public L getLeft() { + return left; + } + + /** + * @return The right side of the pair. + */ + public R getRight() { + return right; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair pair = (Pair) o; + return left.equals(pair.left) && right.equals(pair.right); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{left, right}); + } + + @SuppressWarnings("unchecked") + @Override + public int compareTo(Pair o) { + if (left instanceof Comparable) { + int l = ((Comparable) left).compareTo(o.left); + if (l != 0) { + return l; + } + } + if (right instanceof Comparable) { + int r = ((Comparable) right).compareTo(o.right); + if (r != 0) { + return r; + } + } + return 0; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/SqlCallable.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/SqlCallable.java new file mode 100644 index 00000000..830ae7b8 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/SqlCallable.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.sql.SQLException; + +/** + * An interface analogous to Callable but constrained so that implementations can only throw SqlException, + * not the more generic Exception. + */ +public interface SqlCallable { + + V call() throws SQLException; + +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/StopWatch.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StopWatch.java new file mode 100644 index 00000000..ce0dddf1 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StopWatch.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.util.concurrent.TimeUnit; + +/** + * Stop watch, inspired by the implementation in the Spring framework. + */ +public class StopWatch { + /** + * The timestamp at which the stopwatch was started. + */ + private long start; + + /** + * The timestamp at which the stopwatch was stopped. + */ + private long stop; + + /** + * Starts the stop watch. + */ + public void start() { + start = nanoTime(); + } + + /** + * Stops the stop watch. + */ + public void stop() { + stop = nanoTime(); + } + + private long nanoTime() { + return System.nanoTime(); + } + + /** + * @return The total run time in millis of the stop watch between start and stop calls. + */ + public long getTotalTimeMillis() { + long duration = stop - start; + return TimeUnit.NANOSECONDS.toMillis(duration); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/StringUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StringUtils.java new file mode 100644 index 00000000..d9e4d555 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StringUtils.java @@ -0,0 +1,566 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Various string-related utilities. + */ +public class StringUtils { + private static final String WHITESPACE_CHARS = " \t\n\f\r"; + + /** + * Prevents instantiation. + */ + private StringUtils() { + // Do nothing. + } + + /** + * Trims or pads (with spaces) this string, so it has this exact length. + * + * @param str The string to adjust. {@code null} is treated as an empty string. + * @param length The exact length to reach. + * @return The adjusted string. + */ + public static String trimOrPad(String str, int length) { + return trimOrPad(str, length, ' '); + } + + /** + * Trims or pads this string, so it has this exact length. + * + * @param str The string to adjust. {@code null} is treated as an empty string. + * @param length The exact length to reach. + * @param padChar The padding character. + * @return The adjusted string. + */ + public static String trimOrPad(String str, int length, char padChar) { + StringBuilder result; + if (str == null) { + result = new StringBuilder(); + } else { + result = new StringBuilder(str); + } + + if (result.length() > length) { + return result.substring(0, length); + } + + while (result.length() < length) { + result.append(padChar); + } + return result.toString(); + } + + /** + * Trims or pads this string, so it has this exact length. + * + * @param str The string to adjust. {@code null} is treated as an empty string. + * @param length The exact length to reach. + * @param padChar The padding character. + * @return The adjusted string. + */ + public static String trimOrLeftPad(String str, int length, char padChar) { + if (str == null) { + str = ""; + } + if (str.length() > length) { + return str.substring(0, length); + } + return leftPad(str, length, padChar); + } + + public static String leftPad(String original, int length, char padChar) { + StringBuilder result = new StringBuilder(original); + while (result.length() < length) { + result.insert(0, padChar); + } + return result.toString(); + } + + /** + * Replaces all sequences of whitespace by a single blank. Ex.: "    " -> " " + * + * @param str The string to analyse. + * @return The input string, with all whitespace collapsed. + */ + public static String collapseWhitespace(String str) { + StringBuilder result = new StringBuilder(); + char previous = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (isCharAnyOf(c, WHITESPACE_CHARS)) { + if (previous != ' ') { + result.append(' '); + } + previous = ' '; + } else { + result.append(c); + previous = c; + } + } + return result.toString(); + } + + /** + * Returns the first n characters from this string, where n = count. If the string is shorter, the entire string + * will be returned. If the string is longer, it will be truncated. + * + * @param str The string to parse. + * @param count The amount of characters to return. + * @return The first n characters from this string, where n = count. + */ + public static String left(String str, int count) { + if (str == null) { + return null; + } + + if (str.length() < count) { + return str; + } + + return str.substring(0, count); + } + + /** + * Replaces all occurrances of this originalToken in this string with this replacementToken. + * + * @param str The string to process. + * @param originalToken The token to replace. + * @param replacementToken The replacement. + * @return The transformed str. + */ + public static String replaceAll(String str, String originalToken, String replacementToken) { + return str.replaceAll(Pattern.quote(originalToken), Matcher.quoteReplacement(replacementToken)); + } + + /** + * Checks whether this string is not {@code null} and not empty. + * + * @param str The string to check. + * @return {@code true} if it has content, {@code false} if it is {@code null} or blank. + */ + public static boolean hasLength(String str) { + return str != null && str.length() > 0; + } + + /** + * Turns this string array in one comma-delimited string. + * + * @param strings The array to process. + * @return The new comma-delimited string. An empty string if {@code strings} is empty. {@code null} if strings is {@code null}. + */ + public static String arrayToCommaDelimitedString(Object[] strings) { + return arrayToDelimitedString(",", strings); + } + + /** + * Turns this string array in one delimited string. + * + * @param delimiter The delimiter to use. + * @param strings The array to process. + * @return The new delimited string. An empty string if {@code strings} is empty. {@code null} if strings is {@code null}. + */ + public static String arrayToDelimitedString(String delimiter, Object[] strings) { + if (strings == null) { + return null; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < strings.length; i++) { + if (i > 0) { + builder.append(delimiter); + } + builder.append(strings[i]); + } + return builder.toString(); + } + + /** + * Checks whether this string isn't {@code null} and contains at least one non-blank character. + * + * @param s The string to check. + * @return {@code true} if it has text, {@code false} if not. + */ + public static boolean hasText(String s) { + return (s != null) && (s.trim().length() > 0); + } + + /** + * Splits this string into an array using these delimiters. + * + * @param str The string to split. + * @param delimiters The delimiters to use. + * @return The resulting array. + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + if (str == null) { + return null; + } + Collection tokens = tokenizeToStringCollection(str, delimiters); + return tokens.toArray(new String[0]); + } + + /** + * Splits this string into a collection using these delimiters. + * + * @param str The string to split. + * @param delimiters The delimiters to use. + * @return The resulting array. + */ + public static List tokenizeToStringCollection(String str, String delimiters) { + if (str == null) { + return null; + } + List tokens = new ArrayList<>(str.length() / 5); + char[] delimiterChars = delimiters.toCharArray(); + int start = 0; + int end = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + boolean delimiter = false; + for (char d : delimiterChars) { + if (c == d) { + tokens.add(str.substring(start, end)); + start = i + 1; + end = start; + delimiter = true; + break; + } + } + if (!delimiter) { + if (i == start && c == ' ') { + start++; + end++; + } + if (i >= start && c != ' ') { + end = i + 1; + } + } + } + if (start < end) { + tokens.add(str.substring(start, end)); + } + return tokens; + } + + /** + * Splits this string into a collection using this delimiter and this group delimiter. + * + * @param str The string to split. + * @param delimiterChar The delimiter to use. + * @param groupDelimiterChar The character to use to delimit groups. + * @return The resulting array. + */ + public static List tokenizeToStringCollection(String str, char delimiterChar, char groupDelimiterChar) { + if (str == null) { + return null; + } + List tokens = new ArrayList<>(str.length() / 5); + int start = 0; + int end = 0; + boolean inGroup = false; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == groupDelimiterChar) { + inGroup = !inGroup; + addToken(tokens, str, start, end); + start = i + 1; + end = start; + } else if (!inGroup && c == delimiterChar) { + addToken(tokens, str, start, end); + start = i + 1; + end = start; + } else if (i == start && c == ' ') { + start++; + end++; + } else if (i >= start && c != ' ') { + end = i + 1; + } + } + addToken(tokens, str, start, end); + return tokens; + } + + private static void addToken(List tokens, String str, int start, int end) { + if (start < end) { + tokens.add(str.substring(start, end)); + } + } + + /** + * Counts the number of occurrences of this token in this string. + * + * @param str The string to analyse. + * @param token The token to look for. + * @return The number of occurrences. + */ + public static int countOccurrencesOf(String str, String token) { + if (str == null || token == null || str.length() == 0 || token.length() == 0) { + return 0; + } + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(token, pos)) != -1) { + ++count; + pos = idx + token.length(); + } + return count; + } + + /** + * Replace all occurences of a substring within a string with + * another string. + * + * @param inString String to examine + * @param oldPattern String to replace + * @param newPattern String to insert + * @return a String with the replacements + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + StringBuilder sb = new StringBuilder(); + int pos = 0; // our position in the old string + int index = inString.indexOf(oldPattern); + // the index of an occurrence we've found, or -1 + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString, pos, index); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + sb.append(inString.substring(pos)); + // remember to append any characters to the right of a match + return sb.toString(); + } + + /** + * Convenience method to return a Collection as a comma-delimited + * String. E.g. useful for {@code toString()} implementations. + * + * @param collection the Collection to analyse + * @return The comma-delimited String. + */ + public static String collectionToCommaDelimitedString(Collection collection) { + return collectionToDelimitedString(collection, ", "); + } + + /** + * Convenience method to return a Collection as a delimited + * String. E.g. useful for {@code toString()} implementations. + * + * @param collection the Collection to analyse + * @param delimiter The delimiter. + * @return The delimited String. + */ + public static String collectionToDelimitedString(Collection collection, String delimiter) { + if (collection == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator it = collection.iterator(); + while (it.hasNext()) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append(delimiter); + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder buf = new StringBuilder(str); + while (buf.length() > 0 && Character.isWhitespace(buf.charAt(0))) { + buf.deleteCharAt(0); + } + return buf.toString(); + } + + /** + * Trim any leading occurrence of this character from the given String. + * + * @param str the String to check. + * @param character The character to trim. + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingCharacter(String str, char character) { + StringBuilder buf = new StringBuilder(str); + while (buf.length() > 0 && character == buf.charAt(0)) { + buf.deleteCharAt(0); + } + return buf.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder buf = new StringBuilder(str); + while (buf.length() > 0 && Character.isWhitespace(buf.charAt(buf.length() - 1))) { + buf.deleteCharAt(buf.length() - 1); + } + return buf.toString(); + } + + /** + * Checks whether this strings both begins with this prefix and ends withs either of these suffixes. + * + * @param str The string to check. + * @param prefix The prefix. + * @param suffixes The suffixes. + * @return {@code true} if it does, {@code false} if not. + */ + public static boolean startsAndEndsWith(String str, String prefix, String... suffixes) { + if (StringUtils.hasLength(prefix) && !str.startsWith(prefix)) { + return false; + } + for (String suffix : suffixes) { + if (str.endsWith(suffix) && (str.length() > (prefix + suffix).length())) { + return true; + } + } + return false; + } + + /** + * Trim the trailing linebreak (if any) from this string. + * + * @param str The string. + * @return The string without trailing linebreak. + */ + public static String trimLineBreak(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder buf = new StringBuilder(str); + while (buf.length() > 0 && isLineBreakCharacter(buf.charAt(buf.length() - 1))) { + buf.deleteCharAt(buf.length() - 1); + } + return buf.toString(); + } + + /** + * Checks whether this character is a linebreak character. + * + * @param ch The character + * @return {@code true} if it is, {@code false} if not. + */ + private static boolean isLineBreakCharacter(char ch) { + return '\n' == ch || '\r' == ch; + } + + /** + * Wrap this string every lineSize characters. + * + * @param str The string to wrap. + * @param lineSize The maximum size of each line. + * @return The wrapped string. + */ + public static String wrap(String str, int lineSize) { + if (str.length() < lineSize) { + return str; + } + + StringBuilder result = new StringBuilder(); + int oldPos = 0; + for (int pos = lineSize; pos < str.length(); pos += lineSize) { + result.append(str, oldPos, pos).append("\n"); + oldPos = pos; + } + result.append(str.substring(oldPos)); + return result.toString(); + } + + /** + * Wrap this string at the word boundary at or below lineSize characters. + * + * @param str The string to wrap. + * @param lineSize The maximum size of each line. + * @return The word-wrapped string. + */ + public static String wordWrap(String str, int lineSize) { + if (str.length() < lineSize) { + return str; + } + + StringBuilder result = new StringBuilder(); + int oldPos = 0; + int pos = lineSize; + while (pos < str.length()) { + if (Character.isWhitespace(str.charAt(pos))) { + pos++; + continue; + } + + String part = str.substring(oldPos, pos); + int spacePos = part.lastIndexOf(' '); + if (spacePos > 0) { + pos = oldPos + spacePos + 1; + } + + result.append(str.substring(oldPos, pos).trim()).append("\n"); + oldPos = pos; + pos += lineSize; + } + result.append(str.substring(oldPos)); + return result.toString(); + } + + /** + * Checks whether this characters matches any of these characters. + * + * @param c The char to check. + * @param chars The chars that should match. + * @return {@code true} if it does, {@code false if not}. + */ + public static boolean isCharAnyOf(char c, String chars) { + for (int i = 0; i < chars.length(); i++) { + if (chars.charAt(i) == c) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/TimeFormat.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/TimeFormat.java new file mode 100644 index 00000000..f287584a --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/TimeFormat.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +/** + * Formats execution times. + */ +public class TimeFormat { + /** + * Prevent instantiation. + */ + private TimeFormat() { + // Do nothing + } + + /** + * Formats this execution time as minutes:seconds.millis. Ex.: 02:15.123s + * + * @param millis The number of millis. + * @return The execution in a human-readable format. + */ + public static String format(long millis) { + return String.format("%02d:%02d.%03ds", millis / 60000, (millis % 60000) / 1000, (millis % 1000)); + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/UrlUtils.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/UrlUtils.java new file mode 100644 index 00000000..6164e93c --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/UrlUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.core.internal.util; + +import org.flywaydb.core.api.FlywayException; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; + +/** + * Collection of utility methods for working with URLs. + */ +public class UrlUtils { + /** + * Prevent instantiation. + */ + private UrlUtils() { + // Do nothing + } + + /** + * Retrieves the file path of this URL, with any trailing slashes removed. + * + * @param url The URL to get the file path for. + * @return The file path. + */ + public static String toFilePath(URL url) { + String filePath = new File(decodeURL(url.getPath().replace("+", "%2b"))).getAbsolutePath(); + if (filePath.endsWith("/")) { + return filePath.substring(0, filePath.length() - 1); + } + return filePath; + } + + /** + * Decodes this UTF-8 encoded URL. + * + * @param url The url to decode. + * @return The decoded URL. + */ + public static String decodeURL(String url) { + try { + return URLDecoder.decode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Can never happen", e); + } + } +} \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/internal/util/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/internal/util/package-info.java new file mode 100644 index 00000000..bc4ea6f4 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/internal/util/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.core.internal.util; \ No newline at end of file diff --git a/flyway-core/src/main/java/org/flywaydb/core/package-info.java b/flyway-core/src/main/java/org/flywaydb/core/package-info.java new file mode 100644 index 00000000..e54ea486 --- /dev/null +++ b/flyway-core/src/main/java/org/flywaydb/core/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The main Flyway package and for most users, the only one they'll ever need to know about. + */ +package org.flywaydb.core; \ No newline at end of file diff --git a/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt b/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt new file mode 100644 index 00000000..17851514 --- /dev/null +++ b/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt @@ -0,0 +1 @@ +${pom.version} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3509efdf..2c6814ee 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ + flyway-core dbswitch-common dbswitch-core dbswitch-product @@ -160,6 +161,35 @@ 3.3.4 + + commons-logging + commons-logging + 1.2 + true + + + + org.jboss + jboss-vfs + 3.2.15.Final + true + + + + org.osgi + org.osgi.core + true + 4.3.1 + provided + + + + com.google.android + android + 4.0.1.2 + true + +