1 /*
2  * Copyright 2010-2020 Redgate Software Ltd
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.flywaydb.core;
17
18 import org.flywaydb.core.api.FlywayException;
19 import org.flywaydb.core.api.MigrationInfoService;
20 import org.flywaydb.core.api.callback.Callback;
21 import org.flywaydb.core.api.configuration.ClassicConfiguration;
22 import org.flywaydb.core.api.configuration.Configuration;
23 import org.flywaydb.core.api.configuration.FluentConfiguration;
24 import org.flywaydb.core.api.logging.Log;
25 import org.flywaydb.core.api.logging.LogFactory;
26 import org.flywaydb.core.api.migration.JavaMigration;
27 import org.flywaydb.core.api.resolver.MigrationResolver;
28 import org.flywaydb.core.internal.callback.*;
29 import org.flywaydb.core.internal.clazz.ClassProvider;
30 import org.flywaydb.core.internal.clazz.NoopClassProvider;
31 import org.flywaydb.core.internal.command.*;
32 import org.flywaydb.core.internal.configuration.ConfigurationValidator;
33 import org.flywaydb.core.internal.database.DatabaseFactory;
34 import org.flywaydb.core.internal.database.base.Database;
35 import org.flywaydb.core.internal.database.base.Schema;
36 import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory;
37 import org.flywaydb.core.internal.license.VersionPrinter;
38 import org.flywaydb.core.internal.parser.ParsingContext;
39 import org.flywaydb.core.internal.resolver.CompositeMigrationResolver;
40 import org.flywaydb.core.internal.resource.NoopResourceProvider;
41 import org.flywaydb.core.internal.resource.ResourceProvider;
42 import org.flywaydb.core.internal.resource.StringResource;
43 import org.flywaydb.core.internal.resource.ResourceNameValidator;
44 import org.flywaydb.core.internal.scanner.LocationScannerCache;
45 import org.flywaydb.core.internal.scanner.ResourceNameCache;
46 import org.flywaydb.core.internal.scanner.Scanner;
47 import org.flywaydb.core.internal.scanner.classpath.ClassPathLocationScanner;
48 import org.flywaydb.core.internal.schemahistory.SchemaHistory;
49 import org.flywaydb.core.internal.schemahistory.SchemaHistoryFactory;
50 import org.flywaydb.core.internal.sqlscript.SqlScript;
51 import org.flywaydb.core.internal.sqlscript.SqlScriptExecutorFactory;
52 import org.flywaydb.core.internal.sqlscript.SqlScriptFactory;
53 import org.flywaydb.core.internal.util.IOUtils;
54 import org.flywaydb.core.internal.util.Pair;
55 import org.flywaydb.core.internal.util.StringUtils;
56
57 import java.sql.Connection;
58 import java.util.*;
59
60 /**
61  * This is the centre point of Flyway, and for most users, the only class they will ever have to deal with.
62  * <p>
63  * It is THE public API from which all important Flyway functions such as clean, validate and migrate can be called.
64  * </p>
65  * <p>To get started all you need to do is create a configured Flyway object and then invoke its principal methods.</p>
66  * <pre>
67  * Flyway flyway = Flyway.configure().dataSource(url, user, password).load();
68  * flyway.migrate();
69  * </pre>
70  * Note that a configured Flyway object is immutable. If you change the configuration you will end up creating a new Flyway
71  * object.
72  * <p>
73  */

74 public class Flyway {
75     private static final Log LOG = LogFactory.getLog(Flyway.class);
76
77     private final ClassicConfiguration configuration;
78
79     /**
80      * Whether the database connection info has already been printed in the logs.
81      */

82     private boolean dbConnectionInfoPrinted;
83
84     /**
85      * Designed so we can fail fast if the configuration is invalid
86      */

87     private ConfigurationValidator  configurationValidator = new ConfigurationValidator();
88
89     /**
90      * Designed so we can fail fast if the SQL file resources are invalid
91      */

92     private ResourceNameValidator resourceNameValidator = new ResourceNameValidator();
93
94     /**
95      * This is your starting point. This creates a configuration which can be customized to your needs before being
96      * loaded into a new Flyway instance using the load() method.
97      * <p>In its simplest form, this is how you configure Flyway with all defaults to get started:</p>
98      * <pre>Flyway flyway = Flyway.configure().dataSource(url, user, password).load();</pre>
99      * <p>After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway
100      * functionality such as migrate() or clean().</p>
101      *
102      * @return A new configuration from which Flyway can be loaded.
103      */

104     public static FluentConfiguration configure() {
105         return new FluentConfiguration();
106     }
107
108     /**
109      * This is your starting point. This creates a configuration which can be customized to your needs before being
110      * loaded into a new Flyway instance using the load() method.
111      * <p>In its simplest form, this is how you configure Flyway with all defaults to get started:</p>
112      * <pre>Flyway flyway = Flyway.configure().dataSource(url, user, password).load();</pre>
113      * <p>After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway
114      * functionality such as migrate() or clean().</p>
115      *
116      * @param classLoader The class loader to use when loading classes and resources.
117      * @return A new configuration from which Flyway can be loaded.
118      */

119     public static FluentConfiguration configure(ClassLoader classLoader) {
120         return new FluentConfiguration(classLoader);
121     }
122
123     /**
124      * Creates a new instance of Flyway with this configuration. In general the Flyway.configure() factory method should
125      * be preferred over this constructor, unless you need to create or reuse separate Configuration objects.
126      *
127      * @param configuration The configuration to use.
128      */

129     public Flyway(Configuration configuration) {
130         this.configuration = new ClassicConfiguration(configuration);
131     }
132
133     /**
134      * @return The configuration that Flyway is using.
135      */

136     public Configuration getConfiguration() {
137         return new ClassicConfiguration(configuration);
138     }
139
140     /**
141      * Used to cache resource names for classpath scanning between commands
142      */

143     private ResourceNameCache resourceNameCache = new ResourceNameCache();
144
145     /**
146      * Used to cache LocationScanners between commands
147      */

148     private final LocationScannerCache locationScannerCache = new LocationScannerCache();
149
150     /**
151      * <p>Starts the database migration. All pending migrations will be applied in order.
152      * Calling migrate on an up-to-date database has no effect.</p>
153      * <img src="https://flywaydb.org/assets/balsamiq/command-migrate.png" alt="migrate">
154      *
155      * @return The number of successfully applied migrations.
156      * @throws FlywayException when the migration failed.
157      */

158     public int migrate() throws FlywayException {
159         return execute(new Command<Integer>() {
160             public Integer execute(MigrationResolver migrationResolver,
161                                    SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor
162
163
164
165             ) {
166                 if (configuration.isValidateOnMigrate()) {
167                     doValidate(database, migrationResolver, schemaHistory, schemas, callbackExecutor,
168                             true // Always ignore pending migrations when validating before migrating
169                     );
170                 }
171
172                 if (!schemaHistory.exists()) {
173                     List<Schema> nonEmptySchemas = new ArrayList<>();
174                     for (Schema schema : schemas) {
175                         if (schema.exists() && !schema.empty()) {
176                             nonEmptySchemas.add(schema);
177                         }
178                     }
179
180                     if (!nonEmptySchemas.isEmpty()) {
181                         if (configuration.isBaselineOnMigrate()) {
182                             doBaseline(schemaHistory, callbackExecutor);
183                         } else {
184                             // Second check for MySQL which is sometimes flaky otherwise
185                             if (!schemaHistory.exists()) {
186                                 throw new FlywayException("Found non-empty schema(s) "
187                                         + StringUtils.collectionToCommaDelimitedString(nonEmptySchemas)
188                                         + " but no schema history table. Use baseline()"
189                                         + " or set baselineOnMigrate to true to initialize the schema history table.");
190                             }
191                         }
192                     } else {
193                         new DbSchemas(database, schemas, schemaHistory).create(false);
194                         schemaHistory.create(false);
195                     }
196                 }
197
198                 return new DbMigrate(database, schemaHistory, schemas[0], migrationResolver, configuration,
199                         callbackExecutor).migrate();
200             }
201         }, true);
202     }
203
204     private void doBaseline(SchemaHistory schemaHistory, CallbackExecutor callbackExecutor) {
205         new DbBaseline(schemaHistory, configuration.getBaselineVersion(), configuration.getBaselineDescription(),
206                 callbackExecutor).baseline();
207     }
208
209     /**
210      * <p>Undoes the most recently applied versioned migration. If target is specified, Flyway will attempt to undo
211      * versioned migrations in the order they were applied until it hits one with a version below the target. If there
212      * is no versioned migration to undo, calling undo has no effect.</p>
213      * <p><i>Flyway Pro and Flyway Enterprise only</i></p>
214      * <img src="https://flywaydb.org/assets/balsamiq/command-undo.png" alt="undo">
215      *
216      * @return The number of successfully undone migrations.
217      * @throws FlywayException when the undo failed.
218      */

219     public int undo() throws FlywayException {
220
221         throw new org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException("undo");
222
223
224
225
226
227
228
229
230
231
232
233
234     }
235
236     /**
237      * <p>Validate applied migrations against resolved ones (on the filesystem or classpath)
238      * to detect accidental changes that may prevent the schema(s) from being recreated exactly.</p>
239      * <p>Validation fails if</p>
240      * <ul>
241      * <li>differences in migration names, types or checksums are found</li>
242      * <li>versions have been applied that aren't resolved locally anymore</li>
243      * <li>versions have been resolved that haven't been applied yet</li>
244      * </ul>
245      *
246      * <img src="https://flywaydb.org/assets/balsamiq/command-validate.png" alt="validate">
247      *
248      * @throws FlywayException when the validation failed.
249      */

250     public void validate() throws FlywayException {
251         execute(new Command<Void>() {
252             public Void execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, Database database,
253                                 Schema[] schemas, CallbackExecutor callbackExecutor
254
255
256
257             ) {
258                 doValidate(database, migrationResolver, schemaHistory, schemas, callbackExecutor,
259                         configuration.isIgnorePendingMigrations());
260                 return null;
261             }
262         }, true);
263     }
264
265     /**
266      * Performs the actual validation. All set up must have taken place beforehand.
267      *
268      * @param database          The database-specific support.
269      * @param migrationResolver The migration resolver;
270      * @param schemaHistory     The schema history table.
271      * @param schemas           The schemas managed by Flyway.
272      * @param callbackExecutor  The callback executor.
273      * @param ignorePending     Whether to ignore pending migrations.
274      */

275     private void doValidate(Database database, MigrationResolver migrationResolver, SchemaHistory schemaHistory,
276                             Schema[] schemas, CallbackExecutor callbackExecutor, boolean ignorePending) {
277         String validationError =
278                 new DbValidate(database, schemaHistory, schemas[0], migrationResolver,
279                         configuration, ignorePending, callbackExecutor).validate();
280
281         if (validationError != null) {
282             if (configuration.isCleanOnValidationError()) {
283                 doClean(database, schemaHistory, schemas, callbackExecutor);
284             } else {
285                 throw new FlywayException("Validate failed: " + validationError);
286             }
287         }
288     }
289
290     private void doClean(Database database, SchemaHistory schemaHistory, Schema[] schemas, CallbackExecutor callbackExecutor) {
291         new DbClean(database, schemaHistory, schemas, callbackExecutor, configuration.isCleanDisabled()).clean();
292     }
293
294     /**
295      * <p>Drops all objects (tables, views, procedures, triggers, ...) in the configured schemas.
296      * The schemas are cleaned in the order specified by the {@code schemas} property.</p>
297      * <img src="https://flywaydb.org/assets/balsamiq/command-clean.png" alt="clean">
298      *
299      * @throws FlywayException when the clean fails.
300      */

301     public void clean() {
302         execute(new Command<Void>() {
303             public Void execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory, Database database,
304                                 Schema[] schemas, CallbackExecutor callbackExecutor
305
306
307
308             ) {
309                 doClean(database, schemaHistory, schemas, callbackExecutor);
310                 return null;
311             }
312         }, false);
313     }
314
315     /**
316      * <p>Retrieves the complete information about all the migrations including applied, pending and current migrations with
317      * details and status.</p>
318      * <img src="https://flywaydb.org/assets/balsamiq/command-info.png" alt="info">
319      *
320      * @return All migrations sorted by version, oldest first.
321      * @throws FlywayException when the info retrieval failed.
322      */

323     public MigrationInfoService info() {
324         return execute(new Command<MigrationInfoService>() {
325             public MigrationInfoService execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory,
326                                                 final Database database, final Schema[] schemas, CallbackExecutor callbackExecutor
327
328
329
330             ) {
331                 return new DbInfo(migrationResolver, schemaHistory, configuration, callbackExecutor).info();
332             }
333         }, true);
334     }
335
336     /**
337      * <p>Baselines an existing database, excluding all migrations up to and including baselineVersion.</p>
338      *
339      * <img src="https://flywaydb.org/assets/balsamiq/command-baseline.png" alt="baseline">
340      *
341      * @throws FlywayException when the schema baselining failed.
342      */

343     public void baseline() throws FlywayException {
344         execute(new Command<Void>() {
345             public Void execute(MigrationResolver migrationResolver,
346                                 SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor
347
348
349
350             ) {
351                 new DbSchemas(database, schemas, schemaHistory).create(true);
352                 doBaseline(schemaHistory, callbackExecutor);
353                 return null;
354             }
355         }, false);
356     }
357
358     /**
359      * Repairs the Flyway schema history table. This will perform the following actions:
360      * <ul>
361      * <li>Remove any failed migrations on databases without DDL transactions (User objects left behind must still be cleaned up manually)</li>
362      * <li>Realign the checksums, descriptions and types of the applied migrations with the ones of the available migrations</li>
363      * </ul>
364      * <img src="https://flywaydb.org/assets/balsamiq/command-repair.png" alt="repair">
365      *
366      * @throws FlywayException when the schema history table repair failed.
367      */

368     public void repair() throws FlywayException {
369         execute(new Command<Void>() {
370             public Void execute(MigrationResolver migrationResolver,
371                                 SchemaHistory schemaHistory, Database database, Schema[] schemas, CallbackExecutor callbackExecutor
372
373
374
375             ) {
376                 new DbRepair(database, migrationResolver, schemaHistory, callbackExecutor, configuration).repair();
377                 return null;
378             }
379         }, true);
380     }
381
382     /**
383      * Creates the MigrationResolver.
384      *
385      * @param resourceProvider The resource provider.
386      * @param classProvider    The class provider.
387      * @param sqlScriptFactory The SQL statement builder factory.
388      * @param parsingContext   The parsing context.
389      * @return A new, fully configured, MigrationResolver instance.
390      */

391     private MigrationResolver createMigrationResolver(ResourceProvider resourceProvider,
392                                                       ClassProvider<JavaMigration> classProvider,
393                                                       SqlScriptExecutorFactory sqlScriptExecutorFactory,
394                                                       SqlScriptFactory sqlScriptFactory,
395                                                       ParsingContext parsingContext) {
396         return new CompositeMigrationResolver(resourceProvider, classProvider, configuration,
397                 sqlScriptExecutorFactory, sqlScriptFactory, parsingContext, configuration.getResolvers());
398     }
399
400     /**
401      * Executes this command with proper resource handling and cleanup.
402      *
403      * @param command The command to execute.
404      * @param <T>     The type of the result.
405      * @return The result of the command.
406      */

407     /*private -> testing*/ <T> T execute(Command<T> command, boolean scannerRequired) {
408         T result;
409
410         VersionPrinter.printVersion(
411
412
413
414         );
415
416         configurationValidator.validate(configuration);
417
418
419
420
421
422
423
424
425
426
427
428
429         final ResourceProvider resourceProvider;
430         ClassProvider<JavaMigration> classProvider;
431         if (!scannerRequired && configuration.isSkipDefaultResolvers() && configuration.isSkipDefaultCallbacks()) {
432             resourceProvider = NoopResourceProvider.INSTANCE;
433             //noinspection unchecked
434             classProvider = NoopClassProvider.INSTANCE;
435         } else {
436             Scanner<JavaMigration> scanner = new Scanner<>(
437                     JavaMigration.class,
438                     Arrays.asList(configuration.getLocations()),
439                     configuration.getClassLoader(),
440                     configuration.getEncoding()
441
442
443
444                     , resourceNameCache
445                     , locationScannerCache
446             );
447             resourceProvider = scanner;
448             classProvider = scanner;
449         }
450
451         if (configuration.isValidateMigrationNaming()) {
452             resourceNameValidator.validateSQLMigrationNaming(resourceProvider, configuration);
453         }
454
455         JdbcConnectionFactory jdbcConnectionFactory = new JdbcConnectionFactory(configuration.getDataSource(),
456                 configuration.getConnectRetries()
457
458
459
460
461         );
462
463         final ParsingContext parsingContext = new ParsingContext();
464         final SqlScriptFactory sqlScriptFactory =
465                 DatabaseFactory.createSqlScriptFactory(jdbcConnectionFactory, configuration, parsingContext);
466
467         final SqlScriptExecutorFactory noCallbackSqlScriptExecutorFactory = DatabaseFactory.createSqlScriptExecutorFactory(
468                 jdbcConnectionFactory
469
470
471
472
473         );
474
475         jdbcConnectionFactory.setConnectionInitializer(new JdbcConnectionFactory.ConnectionInitializer() {
476             @Override
477             public void initialize(JdbcConnectionFactory jdbcConnectionFactory, Connection connection) {
478                 if (configuration.getInitSql() == null) {
479                     return;
480                 }
481                 StringResource resource = new StringResource(configuration.getInitSql());
482
483                 SqlScript sqlScript = sqlScriptFactory.createSqlScript(resource, true, resourceProvider);
484                 noCallbackSqlScriptExecutorFactory.createSqlScriptExecutor(connection
485
486
487
488                 ).execute(sqlScript);
489             }
490         });
491
492         Database database = null;
493         try {
494             database = DatabaseFactory.createDatabase(configuration, !dbConnectionInfoPrinted, jdbcConnectionFactory
495
496
497
498             );
499
500             dbConnectionInfoPrinted = true;
501             LOG.debug("DDL Transactions Supported: " + database.supportsDdlTransactions());
502
503             Pair<Schema, List<Schema>> schemas = prepareSchemas(database);
504             Schema defaultSchema = schemas.getLeft();
505
506
507
508
509
510
511
512             parsingContext.populate(database, configuration);
513
514             database.ensureSupported();
515
516             DefaultCallbackExecutor callbackExecutor = new DefaultCallbackExecutor(configuration, database, defaultSchema,
517                     prepareCallbacks(database, resourceProvider, jdbcConnectionFactory, sqlScriptFactory
518
519
520
521                     ));
522
523             SqlScriptExecutorFactory sqlScriptExecutorFactory = DatabaseFactory.createSqlScriptExecutorFactory(jdbcConnectionFactory
524
525
526
527
528             );
529
530             result = command.execute(
531                     createMigrationResolver(resourceProvider, classProvider, sqlScriptExecutorFactory, sqlScriptFactory, parsingContext),
532                     SchemaHistoryFactory.getSchemaHistory(configuration, noCallbackSqlScriptExecutorFactory, sqlScriptFactory,
533                             database, defaultSchema
534
535
536
537                     ),
538                     database,
539                     schemas.getRight().toArray(new Schema[0]),
540                     callbackExecutor
541
542
543
544             );
545         } finally {
546             IOUtils.close(database);
547
548
549
550             showMemoryUsage();
551         }
552         return result;
553     }
554
555     private void showMemoryUsage() {
556         Runtime runtime = Runtime.getRuntime();
557         long free = runtime.freeMemory();
558         long total = runtime.totalMemory();
559         long used = total - free;
560
561         long totalMB = total / (1024 * 1024);
562         long usedMB = used / (1024 * 1024);
563         LOG.debug("Memory usage: " + usedMB + " of " + totalMB + "M");
564     }
565
566     private Pair<Schema, List<Schema>> prepareSchemas(Database database) {
567         String defaultSchemaName = configuration.getDefaultSchema();
568         String[] schemaNames = configuration.getSchemas();
569
570         if (!isDefaultSchemaValid(defaultSchemaName, schemaNames)) {
571             throw new FlywayException("The defaultSchema property is specified but is not a member of the schemas property");
572         }
573
574         LOG.debug("Schemas: " + StringUtils.arrayToCommaDelimitedString(schemaNames));
575         LOG.debug("Default schema: " + defaultSchemaName);
576
577         List<Schema> schemas = new ArrayList<>();
578
579         if (schemaNames.length == 0) {
580             Schema currentSchema = database.getMainConnection().getCurrentSchema();
581             if (currentSchema == null) {
582                 throw new FlywayException("Unable to determine schema for the schema history table." +
583                         " Set a default schema for the connection or specify one using the defaultSchema property!");
584             }
585             schemas.add(currentSchema);
586         } else {
587             for (String schemaName : schemaNames) {
588                 schemas.add(database.getMainConnection().getSchema(schemaName));
589             }
590         }
591
592         if (defaultSchemaName == null && schemaNames.length > 0) {
593             defaultSchemaName = schemaNames[0];
594         }
595
596         Schema defaultSchema = (defaultSchemaName != null)
597                 ? database.getMainConnection().getSchema(defaultSchemaName)
598                 : database.getMainConnection().getCurrentSchema();
599
600         return Pair.of(defaultSchema, schemas);
601     }
602
603     private boolean isDefaultSchemaValid(String defaultSchema, String[] schemas) {
604         // No default schema specified
605         if (defaultSchema == null) {
606             return true;
607         }
608         // Default schema is one of those Flyway is managing
609         for (String schema : schemas) {
610             if (defaultSchema.equals(schema)) {
611                 return true;
612             }
613         }
614         return false;
615     }
616
617     private List<Callback> prepareCallbacks(Database database, ResourceProvider resourceProvider,
618                                             JdbcConnectionFactory jdbcConnectionFactory,
619                                             SqlScriptFactory sqlScriptFactory
620
621
622
623
624     ) {
625         List<Callback> effectiveCallbacks = new ArrayList<>();
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644         effectiveCallbacks.addAll(Arrays.asList(configuration.getCallbacks()));
645
646         if (!configuration.isSkipDefaultCallbacks()) {
647             SqlScriptExecutorFactory sqlScriptExecutorFactory =
648                     DatabaseFactory.createSqlScriptExecutorFactory(jdbcConnectionFactory
649
650
651
652
653                     );
654
655             effectiveCallbacks.addAll(
656                     new SqlScriptCallbackFactory(
657                             resourceProvider,
658                             sqlScriptExecutorFactory,
659                             sqlScriptFactory,
660                             configuration
661                     ).getCallbacks());
662         }
663
664
665
666
667
668         return effectiveCallbacks;
669     }
670
671     /**
672      * A Flyway command that can be executed.
673      *
674      * @param <T> The result type of the command.
675      */

676     /*private -> testing*/ interface Command<T> {
677         /**
678          * Execute the operation.
679          *
680          * @param migrationResolver The migration resolver to use.
681          * @param schemaHistory     The schema history table.
682          * @param database          The database-specific support for these connections.
683          * @param schemas           The schemas managed by Flyway.
684          * @param callbackExecutor  The callback executor.
685          * @return The result of the operation.
686          */

687         T execute(MigrationResolver migrationResolver, SchemaHistory schemaHistory,
688                   Database database, Schema[] schemas, CallbackExecutor callbackExecutor
689
690
691
692         );
693     }
694 }