diff --git b/.gitattributes a/.gitattributes new file mode 100644 index 0000000..a54bdaf --- /dev/null +++ a/.gitattributes @@ -0,0 +1,10 @@ +* text eol=lf + +*.ico binary +*.png binary +*.gif binary +*.jar binary +*.nofilter binary +*.kdbx binary +*.exe binary +*.dll binary \ No newline at end of file diff --git b/.gitignore a/.gitignore new file mode 100644 index 0000000..7aaf979 --- /dev/null +++ a/.gitignore @@ -0,0 +1,21 @@ +bin +build +target +*.iml +*.ipr +*.iws +*.ids +.project +.classpath +.settings +.metadata +.gradle +*.h2.db +.DS_Store + +**/.idea/** +!**/.idea/runConfigurations/ +!**/.idea/runConfigurations/* +!**/.idea/codeStyles/ +!**/.idea/codeStyles/* +flyway-core/SPOOLEDCONTENTS.SQL diff --git b/.travis.yml a/.travis.yml new file mode 100644 index 0000000..ce3679e --- /dev/null +++ a/.travis.yml @@ -0,0 +1,13 @@ +language: minimal + +env: + matrix: + - JDK_VERSION=10 + - JDK_VERSION=11 + - JDK_VERSION=12 + - JDK_VERSION=13 + +install: skip + +script: + - docker run -it --rm -v "$PWD":/usr/src/flyway -w /usr/src/flyway maven:3-jdk-$JDK_VERSION mvn install -e \ No newline at end of file diff --git b/CONTRIBUTING.md a/CONTRIBUTING.md new file mode 100644 index 0000000..c6f8bb7 --- /dev/null +++ a/CONTRIBUTING.md @@ -0,0 +1,8 @@ +Contribute +========== + +So you've got an awesome idea to throw into Flyway. Great! Please keep the following in mind: + +* **Use http://stackoverflow.com/questions/tagged/flyway for Flyway questions that are not bugs.** +* **Contributions must include the necessary documentation updates.** +* **Make sure to read https://flywaydb.org/documentation/contribute/ first** \ No newline at end of file diff --git b/ISSUE_TEMPLATE.md a/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..4745099 --- /dev/null +++ a/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +###### Which version and edition of Flyway are you using? + + + +###### If this is not the latest version, can you reproduce the issue with the latest one as well? +(Many bugs are fixed in newer releases and upgrading will often resolve the issue) + + + +###### Which client are you using? (Command-line, Java API, Maven plugin, Gradle plugin) + + + +###### Which database are you using (type & version)? + + + +###### Which operating system are you using? + + + +###### What did you do? +(Please include the content causing the issue, any relevant configuration settings, the SQL statement that failed (if relevant) and the command you ran.) + + + +###### What did you expect to see? + + + +###### What did you see instead? diff --git b/LICENSE a/LICENSE new file mode 100644 index 0000000..fb87ce6 --- /dev/null +++ a/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2010-2020 Redgate Software Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git b/LICENSE.txt a/LICENSE.txt new file mode 100644 index 0000000..547b931 --- /dev/null +++ a/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2010-2020 Redgate Software Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git b/README.md a/README.md new file mode 100644 index 0000000..d06b5d0 --- /dev/null +++ a/README.md @@ -0,0 +1,47 @@ +# [Flyway](https://flywaydb.org) by [Redgate](https://www.red-gate.com/) [![Build Status](https://api.travis-ci.org/flyway/flyway.svg)](https://travis-ci.org/flyway/flyway) [![Maven Central](https://img.shields.io/maven-central/v/org.flywaydb/flyway-core.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.flywaydb.flyway-core%22) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) + +### Database Migrations Made Easy. + +![Flyway](https://flywaydb.org/assets/logo/flyway-logo-tm.png "Flyway") + +#### Evolve your database schema easily and reliably across all your instances. +Simple, focused and powerful. + +#### Works on +Windows, macOS, Linux, Docker, Java and Android + +#### Supported build tools +Maven and Gradle + +#### Supported databases +Oracle, SQL Server, DB2, MySQL, Aurora MySQL, MariaDB, Percona XtraDB Cluster, PostgreSQL, Aurora PostgreSQL, Redshift, CockroachDB, SAP HANA, Sybase ASE, Informix, H2, HSQLDB, Derby, SQLite, Firebird + +#### Third party plugins +SBT, Ant, Spring Boot, Grails, Play!, DropWizard, Grunt, Griffon, Ninja, ... + +## Documentation +https://flywaydb.org + +## About +Flyway is brought to you by [Redgate](https://www.red-gate.com/) with the help of [many contributors](https://flywaydb.org/documentation/contribute/hallOfFame.html). + +## How to contribute +https://flywaydb.org/documentation/contribute + +## License +Copyright (C) 2010-2020 [Boxfuse GmbH](https://boxfuse.com) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +## Trademark +Flyway is a registered trademark of [Boxfuse GmbH](https://boxfuse.com), owned by [Redgate](https://www.red-gate.com/). diff --git b/README.txt a/README.txt new file mode 100644 index 0000000..6bcd0ec --- /dev/null +++ a/README.txt @@ -0,0 +1,34 @@ +Welcome to Flyway. +------------------ +Database Migrations Made Easy. + + +Documentation +------------- +You can find getting started guides and reference documentation at https://flywaydb.org + + +Contributing +------------ +Here is the info on how you can contribute in various ways to the project: https://flywaydb.org/documentation/contribute/ + + +License +------- +Copyright (C) 2010-2020 Boxfuse GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + +Flyway is a registered trademark of Boxfuse GmbH. \ No newline at end of file diff --git b/flyway-commandline/pom.xml a/flyway-commandline/pom.xml new file mode 100644 index 0000000..418e05e --- /dev/null +++ a/flyway-commandline/pom.xml @@ -0,0 +1,376 @@ + + + 4.0.0 + + org.flywaydb + flyway-parent + 0-SNAPSHOT + + flyway-commandline + jar + ${project.artifactId} + + + + + + + + ${project.groupId} + flyway-core + ${project.version} + + + org.apache.derby + derby + + + org.apache.derby + derbytools + + + org.apache.derby + derbyshared + + + org.apache.derby + derbyclient + + + com.h2database + h2 + + + org.hsqldb + hsqldb + + + com.microsoft.sqlserver + mssql-jdbc + + + net.sourceforge.jtds + jtds + + + mysql + mysql-connector-java + + + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + + + org.firebirdsql.jdbc + jaybird-jdk18 + + + org.mariadb.jdbc + mariadb-java-client + + + org.postgresql + postgresql + + + net.snowflake + snowflake-jdbc + + + org.xerial + sqlite-jdbc + + + org.fusesource.jansi + jansi + + + com.google.code.gson + gson + + + + + + + + + + + + + + + + + + + + + + + + maven-resources-plugin + + + copy-license + + copy-resources + + generate-resources + + + + .. + + LICENSE.txt + README.txt + + + + ${project.build.outputDirectory}/META-INF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + build-assemblies + + false + + + + net.adoptopenjdk + jre + windows-x64 + zip + provided + + + net.adoptopenjdk + jre + linux-x64 + tar.gz + provided + + + net.adoptopenjdk + jre + macos-x64 + tar.gz + provided + + + com.oracle.database.jdbc + ojdbc8 + + + + + + maven-dependency-plugin + + + unpack-jres + package + + unpack-dependencies + + + true + jre + true + true + + + + + copy-editions + package + + copy + + + + + org.flywaydb.pro + flyway-commandline + ${project.version} + jar + ${project.build.directory}/editions/pro + + + org.flywaydb.pro + flyway-core + ${project.version} + jar + ${project.build.directory}/editions/pro + + + org.flywaydb.enterprise + flyway-commandline + ${project.version} + jar + ${project.build.directory}/editions/enterprise + + + org.flywaydb.enterprise + flyway-core + ${project.version} + jar + ${project.build.directory}/editions/enterprise + + + + + + + + + + + + + + + + + + + + + + maven-assembly-plugin + + + nojre-assembly + package + + single + + + + src/main/assembly/no-jre.xml + + false + + + + jre-assemblies + package + + single + + + + src/main/assembly/windows.xml + src/main/assembly/linux.xml + src/main/assembly/macos.xml + + + + + + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/LICENSES-THIRD-PARTY.txt a/flyway-commandline/src/main/assembly/LICENSES-THIRD-PARTY.txt new file mode 100644 index 0000000..d97ebd7 --- /dev/null +++ a/flyway-commandline/src/main/assembly/LICENSES-THIRD-PARTY.txt @@ -0,0 +1,4485 @@ +This distribution contains third-party software subject to various licenses: + +Java Runtime Environment ${version.jre} (jre/*) +------------------------------------- +Source: https://adoptopenjdk.net/ + + +GNU General Public License, version 2, +with the Classpath Exception + +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you +can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must +make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA +BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER +OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., 59 + Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes + with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free + software, and you are welcome to redistribute it under certain conditions; + type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. + + + + +ADDITIONAL INFORMATION ABOUT LICENSING + +Certain files distributed by Oracle America, Inc. and/or its affiliates are +subject to the following clarification and special exception to the GPLv2, +based on the GNU Project exception for its Classpath libraries, known as the +GNU Classpath Exception. + +Note that Oracle includes multiple, independent programs in this software +package. Some of those programs are provided under licenses deemed +incompatible with the GPLv2 by the Free Software Foundation and others. +For example, the package includes programs licensed under the Apache +License, Version 2.0 and may include FreeType. Such programs are licensed +to you under their original licenses. + +Oracle facilitates your further distribution of this package by adding the +Classpath Exception to the necessary parts of its GPLv2 code, which permits +you to use that code in combination with other independent modules not +licensed under the GPLv2. However, note that this would not permit you to +commingle code under an incompatible license with Oracle's GPLv2 licensed +code by, for example, cutting and pasting such code into a file also +containing Oracle's GPLv2 licensed code and then distributing the result. + +Additionally, if you were to remove the Classpath Exception from any of the +files to which it applies and distribute the result, you would likely be +required to license some or all of the other code in that distribution under +the GPLv2 as well, and since the GPLv2 is incompatible with the license terms +of some items included in the distribution by Oracle, removing the Classpath +Exception could therefore effectively compromise your ability to further +distribute the package. + +Failing to distribute notices associated with some files may also create +unexpected legal consequences. + +Proceed with caution and we recommend that you obtain the advice of a lawyer +skilled in open source matters before removing the Classpath Exception or +making modifications to this package which may subsequently be redistributed +and/or involve the use of third party software. + + +Oracle OJDBC8 ${version.oracle} (drivers/ojdbc-${version.oracle}.jar) +---------------------------------------------------------- +Source: https://repo1.maven.org/maven2/com/oracle/ojdbc/ojdbc8/${version.oracle}/ojdbc8-${version.oracle}.pom + +Oracle Free Use Terms and Conditions (FUTC) + +Oracle Free Use Terms and Conditions Definitions + +"Oracle" refers to Oracle America, Inc. "You" and "Your" refers to (a) a company or organization (each an "Entity") +accessing the Programs, if use of the Programs will be on behalf of such Entity; or (b) an individual accessing the +Programs, if use of the Programs will not be on behalf of an Entity. "Program(s)" refers to Oracle software provided +by Oracle pursuant to the following terms and any updates, error corrections, and/or Program Documentation provided by +Oracle. "Program Documentation" refers to Program user manuals and Program installation manuals, if any. If available, +Program Documentation may be delivered with the Programs and/or may be accessed from www.oracle.com/documentation. +"Separate Terms" refers to separate license terms that are specified in the Program Documentation, readmes or notice +files and that apply to Separately Licensed Technology. "Separately Licensed Technology" refers to Oracle or third +party technology that is licensed under Separate Terms and not under the terms of this license. Separately Licensed +Technology Oracle may provide certain notices to You in Program Documentation, readmes or notice files in connection +with Oracle or third party technology provided as or with the Programs. If specified in the Program Documentation, +readmes or notice files, such technology will be licensed to You under Separate Terms. Your rights to use Separately +Licensed Technology under Separate Terms are not restricted in any way by the terms herein. For clarity, +notwithstanding the existence of a notice, third party technology that is not Separately Licensed Technology shall be +deemed part of the Programs licensed to You under the terms of this license. Source Code for Open Source Software For +software that You receive from Oracle in binary form that is licensed under an open source license that gives You the +right to receive the source code for that binary, You can obtain a copy of the applicable source code from +https://oss.oracle.com/sources/ or http://www.oracle.com/goto/opensourcecode. If the source code for such software was +not provided to You with the binary, You can also receive a copy of the source code on physical media by submitting a +written request pursuant to the instructions in the "Written Offer for Source Code" section of the latter website. +------------------------------------------------------------------------------- +The following license terms apply to those Programs that are not provided to You under Separate Terms. License Rights +and Restrictions Oracle grants to You, as a recipient of this Program, a nonexclusive, nontransferable, limited license +to, subject to the conditions stated herein, (a) internally use the unmodified Programs for the purposes of developing, +testing, prototyping and demonstrating your applications, and running the Programs for your own internal business +operations; and (b) redistribute unmodified Programs and Programs Documentation, under the terms of this License, +provided that You do not charge Your end users any additional fees for the use of the Programs. You may make copies of +the Programs to the extent reasonably necessary for exercising the license rights granted herein and for backup +purposes. You are granted the right to use the Programs to provide third party training in the use of the Programs and +associated Separately Licensed Technology only if there is express authorization of such use by Oracle on the Program's +download page or in the Program Documentation. Your license is contingent on Your compliance with the following +conditions: +- You include a copy of this license with any distribution by You of the Programs; +- You do not remove markings or notices of either Oracle's or a licensor's proprietary rights from the Programs or +Program Documentation; +- You comply with all U.S. and applicable export control and economic sanctions laws and regulations that govern Your +use of the Programs (including technical data); +- You do not cause or permit reverse engineering, disassembly or decompilation of the Programs (except as allowed by +law) by You nor allow an associated party to do so. For clarity, any source code that may be included in the +distribution with the Programs is provided solely for reference purposes and may not be modified, unless such source +code is under Separate Terms permitting modification. Ownership Oracle or its licensors retain all ownership and +intellectual property rights to the Programs. Information Collection The Programs' installation and/or auto-update +processes, if any, may transmit a limited amount of data to Oracle or its service provider about those processes to +help Oracle understand and optimize them. Oracle does not associate the data with personally identifiable information. +Refer to Oracle's Privacy Policy at www.oracle.com/privacy. + +Disclaimer of Warranties; Limitation of Liability + +THE PROGRAMS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. ORACLE FURTHER DISCLAIMS ALL WARRANTIES, EXPRESS AND +IMPLIED, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NONINFRINGEMENT. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL ORACLE BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING +BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +Last updated: 8 October 2018 + + +Derby ${version.derby} (drivers/derby-${version.derby}.jar) +Derby Client ${version.derby} (drivers/derbyclient-${version.derby}.jar) +Derby Tools ${version.derby} (drivers/derbytools-${version.derby}.jar) +Derby Shared ${version.derby} (drivers/derbyshared-${version.derby}.jar) +---------------------------------------------------------- +Source: http://db.apache.org/derby/ + +Copyright 2004-2014 The Apache Software Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +H2 ${version.h2} (drivers/h2-${version.h2}.jar) +----------------------------------- +Source: http://www.h2database.com/ + +Copyright 2004-2014 H2 Group + +This software contains unmodified binary redistributions for H2 database engine (http://www.h2database.com/), +which is dual licensed and available under the MPL 2.0 (Mozilla Public License) or under the EPL 1.0 (Eclipse Public License). +An original copy of the license agreement can be found at: http://www.h2database.com/html/license.html + + + + +HSQLDB ${version.hsqldb} (drivers/hsqldb-${version.hsqldb}.jar) +--------------------------------------- +Source: http://hsqldb.org/ + +For work developed by the HSQL Development Group: + +Copyright (c) 2001-2014, The HSQL Development Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of the HSQL Development Group nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +For work originally developed by the Hypersonic SQL Group: + +Copyright (c) 1995-2000, The Hypersonic SQL Group. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of the Hypersonic SQL Group nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP, +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This software consists of voluntary contributions made by many individuals +on behalf of the Hypersonic SQL Group. + + + + +jTDS ${version.jtds} (drivers/jtds-${version.jtds}.jar) +----------------------------------- +Source: http://jtds.sourceforge.net/ + +Copyright (C) 2004-2014 The jTDS Project + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + + + + +Java Native Access ${version.jna} (drivers/jna-${version.jna}.jar, drivers/jna-platform-${version.jna}.jar) +-------------------------------------------------------------------------------- +Source: https://github.com/java-native-access/jna + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + + + +MariaDB Java Client ${version.mariadb} (drivers/mariadb-java-client-${version.mariadb}.jar) +----------------------------------------------------------------- +Source: https://mariadb.com/kb/en/mariadb/client-libraries/mariadb-java-client/ + +Copyright (c) 2012-2014 Monty Program Ab. + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to Monty Program Ab info@montyprogram.com. + +This particular MariaDB Client for Java file is work +derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to +the following copyright and notice provisions: + +Copyright (c) 2009-2011, Marcus Eriksson + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the driver nor the names of its contributors may not be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + + + + +MySQL Connector/J ${version.mysql} (drivers/mysql-connector-java-${version.mysql}.jar) +----------------------------------------------------------------- +Source: https://dev.mysql.com/downloads/connector/j/ + +Introduction + + This License Information User Manual contains Oracle's product license + and other licensing information, including licensing information for + third-party software which may be included in this distribution of + MySQL Connector/J 8.0. + + Last updated: June 2018. + +Licensing Information + + This is a release of MySQL Connector/J 8.0, brought to you by the MySQL + team at Oracle. This software is released under version 2 of the GNU + General Public License (GPLv2), as set forth below, with the following + additional permissions: + + This distribution of MySQL Connector/J 8.0 is distributed with certain + software that is licensed under separate terms, as designated in a + particular file or component or in the license documentation. Without + limiting your rights under the GPLv2, the authors of MySQL hereby grant + you an additional permission to link the program and your derivative + works with the separately licensed software that they have included + with the program. + + Without limiting the foregoing grant of rights under the GPLv2 and + additional permission as to separately licensed software, this + Connector is also subject to the Universal FOSS Exception, version 1.0, + a copy of which is reproduced below and can also be found along with + its FAQ at http://oss.oracle.com/licenses/universal-foss-exception. + + Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights + reserved. + +Election of GPLv2 + + For the avoidance of doubt, except that if any license choice other + than GPL or LGPL is available it will apply instead, Oracle elects to + use only the General Public License version 2 (GPLv2) at this time for + any software where a choice of GPL license versions is made available + with the language indicating that GPLv2 or any later version may be + used, or where a choice of which version of the GPL is applied is + otherwise unspecified. + +GNU General Public License Version 2.0, June 1991 + +The following applies to all products licensed under the GNU General +Public License, Version 2.0: You may not use the identified files +except in compliance with the GNU General Public License, Version +2.0 (the "License.") You may obtain a copy of the License at +http://www.gnu.org/licenses/gpl-2.0.txt. A copy of the license is +also reproduced below. Unless required by applicable law or agreed +to in writing, software distributed under the License is distributed +on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the specific language +governing permissions and limitations under the License. + +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim +copies of this license document, but changing it is not +allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, +and (2) offer you this license which gives you legal permission to +copy, distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, +we want its recipients to know that what they have is not the original, +so that any problems introduced by others will not reflect on the +original authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software + interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as +a special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + + 9. The Free Software Foundation may publish revised and/or new +versions of the General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Program does not specify a +version number of this License, you may choose any version ever +published by the Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the +author to ask for permission. For software which is copyrighted by the +Free Software Foundation, write to the Free Software Foundation; we +sometimes make exceptions for this. Our decision will be guided by the +two goals of preserving the free status of all derivatives of our free +software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS +WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details + type 'show w'. This is free software, and you are welcome + to redistribute it under certain conditions; type 'show c' + for details. + +The hypothetical commands 'show w' and 'show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than 'show w' and +'show c'; they could even be mouse-clicks or menu items--whatever +suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program 'Gnomovision' (which makes passes at compilers) written + by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, +you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use +the GNU Lesser General Public License instead of this License. + +The Universal FOSS Exception, Version 1.0 + + In addition to the rights set forth in the other license(s) included in + the distribution for this software, data, and/or documentation + (collectively the "Software", and such licenses collectively with this + additional permission the "Software License"), the copyright holders + wish to facilitate interoperability with other software, data, and/or + documentation distributed with complete corresponding source under a + license that is OSI-approved and/or categorized by the FSF as free + (collectively "Other FOSS"). We therefore hereby grant the following + additional permission with respect to the use and distribution of the + Software with Other FOSS, and the constants, function signatures, data + structures and other invocation methods used to run or interact with + each of them (as to each, such software's "Interfaces"): + i. The Software's Interfaces may, to the extent permitted by the + license of the Other FOSS, be copied into, used and distributed in + the Other FOSS in order to enable interoperability, without + requiring a change to the license of the Other FOSS other than as + to any Interfaces of the Software embedded therein. The Software's + Interfaces remain at all times under the Software License, + including without limitation as used in the Other FOSS (which upon + any such use also then contains a portion of the Software under the + Software License). + ii. The Other FOSS's Interfaces may, to the extent permitted by the + license of the Other FOSS, be copied into, used and distributed in + the Software in order to enable interoperability, without requiring + that such Interfaces be licensed under the terms of the Software + License or otherwise altering their original terms, if this does + not require any portion of the Software other than such Interfaces + to be licensed under the terms other than the Software License. + iii. If only Interfaces and no other code is copied between the + Software and the Other FOSS in either direction, the use and/or + distribution of the Software with the Other FOSS shall not be + deemed to require that the Other FOSS be licensed under the license + of the Software, other than as to any Interfaces of the Software + copied into the Other FOSS. This includes, by way of example and + without limitation, statically or dynamically linking the Software + together with Other FOSS after enabling interoperability using the + Interfaces of one or both, and distributing the resulting + combination under different licenses for the respective portions + thereof. For avoidance of doubt, a license which is OSI-approved or + categorized by the FSF as free, includes, for the purpose of this + permission, such licenses with additional permissions, and any + license that has previously been so approved or categorized as + free, even if now deprecated or otherwise no longer recognized as + approved or free. Nothing in this additional permission grants any + right to distribute any portion of the Software on terms other than + those of the Software License or grants any additional permission + of any kind for use or distribution of the Software in conjunction + with software other than Other FOSS. + +Licenses for Third-Party Components + + The following sections contain licensing information for libraries that + we have included with the MySQL Connector/J 8.0 source and components + used to test MySQL Connector/J 8.0. Commonly used licenses referenced + herein can be found in Commonly Used Licenses. We are thankful to all + individuals that have created these. + +Ant-Contrib + + The following software may be included in this product: +Ant-Contrib +Copyright (c) 2001-2003 Ant-Contrib project. All rights reserved. +Licensed under the Apache 1.1 License Agreement, a copy of which is reproduced b +elow. + +The Apache Software License, Version 1.1 + +Copyright (c) 2001-2003 Ant-Contrib project. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + + 3. The end-user documentation included with the redistribution, if + any, must include the following acknowlegement: + "This product includes software developed by the + Ant-Contrib project (http://sourceforge.net/projects/ant-contrib)." + Alternately, this acknowlegement may appear in the software itself, + if and wherever such third-party acknowlegements normally appear. + + + 4. The name Ant-Contrib must not be used to endorse or promote + products derived from this software without prior written + permission. For written permission, please contact + ant-contrib-developers@lists.sourceforge.net. + + + 5. Products derived from this software may not be called "Ant-Contrib" + nor may "Ant-Contrib" appear in their names without prior written + permission of the Ant-Contrib project. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE ANT-CONTRIB PROJECT OR ITS + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +c3p0 JDBC Library + + You are receiving a copy of c3p0-0.9.1-pre6.jar in both source and + object code in the following /src/lib/c3p0-0.9.1-pre6.jar. The terms of + the Oracle license do NOT apply to c3p0-0.9.1-pre6.jar; it is licensed + under the following license, separately from the Oracle programs you + receive. If you do not wish to install this library, you may remove the + file /src/lib/c3p0-0.9.1-pre6.jar, but the Oracle program might not + operate properly or at all without the library. + + This component is licensed under GNU Lesser General Public License + Version 2.1, February 1999. + +Google Protocol Buffers + + The following software may be included in this product: +Protocol Buffers (aka Google protobuf) + +Google Protocol Buffers - protobuf +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, +with or without modification, are permitted provided +that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. +* Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by +the owner of the input file used when generating it. This +code is not standalone and requires a support library to be +linked with it. This support library is itself covered by +the above license. + +jboss-common-jdbc-wrapper.jar + + You are receiving a copy of jboss-common-jdbc-wrapper.jar in both + source and object code in the following + /src/lib/jboss-common-jdbc-wrapper.jar. The terms of the Oracle license + do NOT apply to jboss-common-jdbc-wrapper.jar; it is licensed under the + following license, separately from the Oracle programs you receive. If + you do not wish to install this library, you may remove the file + /src/lib/jboss-common-jdbc-wrapper.jar, but the Oracle program might + not operate properly or at all without the library. + + This component is licensed under GNU Lesser General Public License + Version 2.1, February 1999. + +NanoXML + + The following software may be included in this product: + + NanoXML + + * Copyright (C) 2000-2002 Marc De Scheemaecker, All Rights Reserved. + * + + * This software is provided 'as-is', without any express or implied warranty. + + * In no event will the authors be held liable for any damages arising from the + + * use of this software. + * + + * Permission is granted to anyone to use this software for any purpose, + + * including commercial applications, and to alter it and redistribute it + + * freely, subject to the following restrictions: + * + + * 1. The origin of this software must not be misrepresented; you must not + + * claim that you wrote the original software. If you use this software in + + * a product, an acknowledgment in the product documentation would be + + * appreciated but is not required. + * + + * 2. Altered source versions must be plainly marked as such, and must not be + + * misrepresented as being the original software. + * + + * 3. This notice may not be removed or altered from any source distribution. + * + +rox.jar + + The following software may be included in this product: + + rox.jar +Copyright (c) 2006, James Greenfield +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + + * Redistributions of source code must retain the above copyright notice, thi +s + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIE +D +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVI +CES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Simple Logging Facade for Java (SLF4J) + + The following software may be included in this product: +Simple Logging Facade for Java (SLF4J) + +Copyright (c) 2004-2008 QOS.ch +All rights reserved. + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software +and associated documentation files (the "Software"), +to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY +OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Unicode Data Files + + The following software may be included in this product: + + Unicode Data Files +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1991-2014 Unicode, Inc. All rights reserved. Distributed under +the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the Unicode data files and any associated documentation (the "Data Files") +or Unicode software and any associated documentation (the "Software") to deal +in the Data Files or Software without restriction, including without +limitation the rights to use, copy, modify, merge, publish, distribute, +and/or sell copies of the Data Files or Software, and to permit persons to +whom the Data Files or Software are furnished to do so, provided that (a) the +above copyright notice(s) and this permission notice appear with all copies +of the Data Files or Software, (b) both the above copyright notice(s) and +this permission notice appear in associated documentation, and (c) there is +clear notice in each modified Data File or in the Software as well as in the +documentation associated with the Data File(s) or Software that the data or +software has been modified. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS +INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR +CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE +DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written authorization +of the copyright holder. + +Commonly Used Licenses + +Artistic License (Perl) 1.0 + +The "Artistic License" + +Preamble + +The intent of this document is to state the conditions under which a +Package may be copied, such that the Copyright Holder maintains some +semblance of artistic control over the development of the package, +while giving the users of the package the right to use and distribute +the Package in a more-or-less customary fashion, plus the right to make +reasonable modifications. + +Definitions: + + "Package" refers to the collection of files distributed by the + Copyright Holder, and derivatives of that collection of files + created through textual modification. + + "Standard Version" refers to such a Package if it has not been + modified, or has been modified in accordance with the wishes + of the Copyright Holder as specified below. + + "Copyright Holder" is whoever is named in the copyright or + copyrights for the package. + + "You" is you, if you're thinking about copying or distributing + this Package. + + "Reasonable copying fee" is whatever you can justify on the + basis of media cost, duplication charges, time of people involved, + and so on. (You will not be required to justify it to the + Copyright Holder, but only to the computing community at large + as a market that must bear the fee.) + + "Freely Available" means that no fee is charged for the item + itself, though there may be fees involved in handling the item. + It also means that recipients of the item may redistribute it + under the same conditions they received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications +derived from the Public Domain or from the Copyright Holder. A Package +modified in such a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided +that you insert a prominent notice in each changed file stating how and +when you changed that file, and provided that you do at least ONE of the +following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or + an equivalent medium, or placing the modifications on a major archive + site such as uunet.uu.net, or by allowing the Copyright Holder to include + your modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided, and provide + a separate manual page for each non-standard executable that clearly + documents how it differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or +executable form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where + to get the Standard Version. + + b) accompany the distribution with the machine-readable source of + the Package with your modifications. + + c) give non-standard executables non-standard names, and clearly + document the differences in manual pages (or equivalent), together + with instructions on where to get the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this +Package. You may not charge a fee for this Package itself. However, +you may distribute this Package in aggregate with other (possibly +commercial) programs as part of a larger (possibly commercial) software +distribution provided that you do not advertise this Package as a +product of your own. You may embed this Package's interpreter within +an executable of yours (by linking); this shall be construed as a mere +form of aggregation, provided that the complete Standard Version of the +interpreter is so embedded. + +6. The scripts and library files supplied as input to or produced as +output from the programs of this Package do not automatically fall +under the copyright of this Package, but belong to whoever generated +them, and may be sold commercially, and may be aggregated with this +Package. If such scripts or library files are aggregated with this +Package via the so-called "undump" or "unexec" methods of producing a +binary executable image, then distribution of such an image shall +neither be construed as a distribution of this Package nor shall it +fall under the restrictions of Paragraphs 3 and 4, provided that you do +not represent such an executable image as a Standard Version of this +Package. + +7. C subroutines (or comparably compiled subroutines in other +languages) supplied by you and linked into this Package in order to +emulate subroutines and variables of the language defined by this +Package shall not be considered part of this Package, but are the +equivalent of input as in Paragraph 6, provided these subroutines do +not change the language in any way that would cause it to fail the +regression tests for the language. + +8. Aggregation of this Package with a commercial distribution is always +permitted provided that the use of this Package is embedded; that is, +when no overt attempt is made to make this Package's interfaces visible +to the end user of the commercial distribution. Such use shall not be +construed as a distribution of this Package. + +9. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + + The End + +GNU Lesser General Public License Version 2.1, February 1999 + +The following applies to all products licensed under the +GNU Lesser General Public License, Version 2.1: You may +not use the identified files except in compliance with +the GNU Lesser General Public License, Version 2.1 (the +"License"). You may obtain a copy of the License at +http://www.gnu.org/licenses/lgpl-2.1.html. A copy of the +license is also reproduced below. Unless required by +applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +or implied. See the License for the specific language governing +permissions and limitations under the License. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs +must be allowed to use the library. A more frequent case is that +a free library does the same job as widely used non-free libraries. +In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended +to apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + + To apply these terms, attach the following notices to the library. +It is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James + Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + +GNU Lesser General Public License Version 2, June 1991 + +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1991 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 +because it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public Licenses are +intended to guarantee your freedom to share and change free software--to make +sure the software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries +whose authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These +restrictions translate to certain responsibilities for you if you distribute +copies of the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a +fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link a +program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making +changes to the library and recompiling it. And you must show them these terms +so they know their rights. + +Our method of protecting your rights has two steps: (1) copyright the +library, and (2) offer you this license which gives you legal permission to +copy, distribute and/or modify the library. + +Also, for each distributor's protection, we want to make certain that +everyone understands that there is no warranty for this free library. If the +library is modified by someone else and passed on, we want its recipients to +know that what they have is not the original version, so that any problems +introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will +individually obtain patent licenses, thus in effect transforming the program +into proprietary software. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This +license, the GNU Library General Public License, applies to certain +designated libraries. This license is quite different from the ordinary one; +be sure to read it in full, and don't assume that anything in it is the same +as in the ordinary license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to +running a utility program or application program. However, in a textual and +legal sense, the linked executable is a combined work, a derivative of the +original library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public +License for libraries did not effectively promote software sharing, because +most developers did not use the libraries. We concluded that weaker +conditions might promote sharing better. + +However, unrestricted linking of non-free programs would deprive the users of +those programs of all benefit from the free status of the libraries +themselves. This Library General Public License is intended to permit +developers of non-free programs to use free libraries, while preserving your +freedom as a user of such programs to change the free libraries that are +incorporated in them. (We have not seen how to achieve this as regards +changes in header files, but we have achieved it as regards changes in the +actual functions of the Library.) The hope is that this will lead to faster +development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it may +be distributed under the terms of this Library General Public License (also +called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so +as to be conveniently linked with application programs (which use some of +those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means +either the Library or any derivative work under copyright law: that is to +say, a work containing the Library or a portion of it, either verbatim or +with modifications and/or translated straightforwardly into another language. +(Hereinafter, translation is included without limitation in the term +"modification".) + +"Source code" for a work means the preferred form of the work for making +modifications to it. For a library, complete source code means all the source +code for all modules it contains, plus any associated interface definition +files, plus the scripts used to control compilation and installation of the +library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is +covered only if its contents constitute a work based on the Library +(independent of the use of the Library in a tool for writing it). Whether +that is true depends on what the Library does and what the program that uses +the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete +source code as you receive it, in any medium, provided that you conspicuously +and appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this +License and to the absence of any warranty; and distribute a copy of this +License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such +modifications or work under the terms of Section 1 above, provided that you +also meet all of these conditions: + + a) The modified work must itself be a software library. + b) You must cause the files modified to carry prominent notices stating +that you changed the files and the date of any change. + c) You must cause the whole of the work to be licensed at no charge to +all third parties under the terms of this License. + d) If a facility in the modified Library refers to a function or a table +of data to be supplied by an application program that uses the facility, +other than as an argument passed when the facility is invoked, then you must +make a good faith effort to ensure that, in the event an application does not +supply such function or table, the facility still operates, and performs +whatever part of its purpose remains meaningful. + + (For example, a function in a library to compute square roots has a +purpose that is entirely well-defined independent of the application. +Therefore, Subsection 2d requires that any application-supplied function or +table used by this function must be optional: if the application does not +supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend to +the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete +corresponding machine-readable source code, which must be distributed under +the terms of Sections 1 and 2 above on a medium customarily used for software +interchange. + +If distribution of object code is made by offering access to copy from a +designated place, then offering equivalent access to copy the source code +from the same place satisfies the requirement to distribute the source code, +even though third parties are not compelled to copy the source along with the +object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, is +called a "work that uses the Library". Such a work, in isolation, is not a +derivative work of the Library, and therefore falls outside the scope of this +License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that is +part of the Library, the object code for the work may be a derivative work of +the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not +precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the +object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are +linked directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a +"work that uses the Library" with the Library to produce a work containing +portions of the Library, and distribute that work under terms of your choice, +provided that the terms permit modification of the work for the customer's +own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is +used in it and that the Library and its use are covered by this License. You +must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library +among them, as well as a reference directing the user to the copy of this +License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable +source code for the Library including whatever changes were used in the work +(which must be distributed under Sections 1 and 2 above); and, if the work is +an executable linked with the Library, with the complete machine-readable +"work that uses the Library", as object code and/or source code, so that the +user can modify the Library and then relink to produce a modified executable +containing the modified Library. (It is understood that the user who changes +the contents of definitions files in the Library will not necessarily be able +to recompile the application to use the modified definitions.) + b) Accompany the work with a written offer, valid for at least three +years, to give the same user the materials specified in Subsection 6a, above, +for a charge no more than the cost of performing this distribution. + c) If distribution of the work is made by offering access to copy from a +designated place, offer equivalent access to copy the above specified +materials from the same place. + d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating +system. Such a contradiction means you cannot use both them and the Library +together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library +side-by-side in a single library together with other library facilities not +covered by this License, and distribute such a combined library, provided +that the separate distribution of the work based on the Library and of the +other library facilities is otherwise permitted, and provided that you do +these two things: + + a) Accompany the combined library with a copy of the same work based on +the Library, uncombined with any other library facilities. This must be +distributed under the terms of the Sections above. + b) Give prominent notice with the combined library of the fact that part +of it is a work based on the Library, and explaining where to find the +accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full +compliance. + +9. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the +Library (or any work based on the Library), you indicate your acceptance of +this License to do so, and all its terms and conditions for copying, +distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the original +licensor to copy, distribute, link with or modify the Library subject to +these terms and conditions. You may not impose any further restrictions on +the recipients' exercise of the rights granted herein. You are not +responsible for enforcing compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot distribute so as to +satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not distribute the +Library at all. For example, if a patent license would not permit +royalty-free redistribution of the Library by all those who receive copies +directly or indirectly through you, then the only way you could satisfy both +it and this License would be to refrain entirely from distribution of the +Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license +practices. Many people have made generous contributions to the wide range of +software distributed through that system in reliance on consistent +application of that system; it is up to the author/donor to decide if he or +she is willing to distribute software through any other system and a licensee +cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Library under this License may add an +explicit geographical distribution limitation excluding those countries, so +that distribution is permitted only in or among countries not thus excluded. +In such case, this License incorporates the limitation as if written in the +body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software +Foundation. If the Library does not specify a license version number, you may +choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the +author to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes make +exceptions for this. Our decision will be guided by the two goals of +preserving the free status of all derivatives of our free software and of +promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO +LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR +THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER +SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General +Public License). + +To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Library General Public +License as published by the Free Software Foundation; either +version 2 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Library General Public License for more details. + +You should have received a copy of the GNU Library General Public +License along with this library; if not, write to the +Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice + +That's all there is to it! + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Written Offer for Source Code + + For any software that you receive from Oracle in binary form which is + licensed under an open source license that gives you the right to + receive the source code for that binary, you can obtain a copy of the + applicable source code by visiting + http://www.oracle.com/goto/opensourcecode. If the source code for the + binary was not provided to you with the binary, you can also receive a + copy of the source code on physical media by submitting a written + request to the address listed below or by sending an email to Oracle + using the following link: + http://www.oracle.com/goto/opensourcecode/request. + Oracle America, Inc. + Attn: Senior Vice President + Development and Engineering Legal + 500 Oracle Parkway, 10th Floor + Redwood Shores, CA 94065 + + Your request should include: + + * The name of the binary for which you are requesting the source code + + * The name and version number of the Oracle product containing the + binary + + * The date you received the Oracle product + + * Your name + + * Your company name (if applicable) + + * Your return mailing address and email, and + + * A telephone number in the event we need to reach you. + + We may charge you a fee to cover the cost of physical media and + processing. + + Your request must be sent + a. within three (3) years of the date you received the Oracle product + that included the binary that is the subject of your request, or + b. in the case of code licensed under the GPL v3 for as long as Oracle + offers spare parts or customer support for that product model. + + + + + + + +PostgreSQL JDBC Driver ${version.postgresql} (drivers/postgresql-${version.postgresql}.jar) +----------------------------------------------------------------------------- +Source: https://github.com/pgjdbc/pgjdbc + +Copyright (c) 1997-2014, PostgreSQL Global Development Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the PostgreSQL Global Development Group nor the names + of its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +Snowflake JDBC ${version.snowflake} (drivers/snowflake-jdbc-${version.snowflake}.jar) +------------------------------------------------------------------ +Source: hhttps://github.com/snowflakedb/snowflake-jdbc/blob/master/LICENSE.txt + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2013-2018 Snowflake Computing, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +SQLite JDBC Driver ${version.sqlite} (drivers/sqlite-jdbc-${version.sqlite}.jar) +---------------------------------------------------------------- +Source: https://bitbucket.org/xerial/sqlite-jdbc + +Copyright 2014 Taro L. Saito + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Copyright (c) 2006, David Crawshaw. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + + +Microsoft SQL Server JDBC Driver ${version.mssql-jdbc} (drivers/mssql-jdbc-${version.mssql-jdbc}.jar) +----------------------------------------------------------------------------- +Source: https://github.com/Microsoft/mssql-jdbc + +Copyright(c) 2017 Microsoft Corporation +All rights reserved. + +MIT License +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + + +Jansi ${version.jansi} (lib/jansi-${version.jansi}.jar) +---------------------------------------------------------------- +Source: https://github.com/fusesource/jansi + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Gson ${version.gson} (lib/gson-${version.gson}.jar) +---------------------------------------------------------------- +Source: https://raw.githubusercontent.com/google/gson/master/LICENSE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/README.txt a/flyway-commandline/src/main/assembly/README.txt new file mode 100644 index 0000000..b4258a6 --- /dev/null +++ a/flyway-commandline/src/main/assembly/README.txt @@ -0,0 +1,25 @@ +Welcome to Flyway +----------------- +Database Migrations Made Easy + + +Documentation +------------- +You can find getting started guides and reference documentation at https://flywaydb.org + + +Contributing +------------ +Here is the info on how you can contribute in various ways to the project: https://flywaydb.org/documentation/contribute/ + + +License +------- +Copyright (C) 2010-2020 Boxfuse GmbH + +Flyway Community Edition : https://flywaydb.org/licenses/flyway-community +Flyway 30 day limited trial : https://flywaydb.org/licenses/flyway-trial +Flyway Pro Edition : https://flywaydb.org/licenses/flyway-pro +Flyway Enterprise Edition : https://flywaydb.org/licenses/flyway-enterprise + +Flyway is a registered trademark of Boxfuse GmbH. \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/component.xml a/flyway-commandline/src/main/assembly/component.xml new file mode 100644 index 0000000..ac3baec --- /dev/null +++ a/flyway-commandline/src/main/assembly/component.xml @@ -0,0 +1,146 @@ + + + + + src/main/assembly + + + flyway.cmd + + 644 + true + + + src/main/assembly + + + flyway + + 744 + unix + true + + + src/main/assembly + drivers + + put-your-jdbc-drivers-here.txt + + 644 + + + src/main/assembly + conf + + flyway.conf + + 644 + + + src/main/assembly + sql + + put-your-sql-migrations-here.txt + + 644 + + + src/main/assembly + jars + + put-your-java-migration-jars-here.txt + + 644 + + + src/main/assembly + licenses + + flyway-community.txt + LICENSES-THIRD-PARTY.txt + + flyway-pro.txt + flyway-enterprise.txt + + + 644 + true + + + src/main/assembly + + + README.txt + + 644 + + + + target/editions/pro + lib/pro + + * + + 644 + + + target/editions/enterprise + lib/enterprise + + * + + 644 + + + + + + lib + + false + + org.fusesource.jansi:jansi + com.google.code.gson:gson + + + + lib/community + + false + + org.flywaydb:flyway-core + org.flywaydb:flyway-commandline + + + + drivers + + false + + * + + + org.flywaydb:flyway-* + org.fusesource.jansi:jansi + com.google.code.gson:gson + + + + \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/flyway a/flyway-commandline/src/main/assembly/flyway new file mode 100644 index 0000000..51c1d10 --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Copyright 2010-2020 Boxfuse GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Detect Linux +linux=false + case "`uname`" in + Linux*) linux=true;; + esac + +# Dereference softlinks +THIS="$0" + while [ -h "$THIS" ] ; do + ls=`ls -ld "$THIS"` + softlink=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$softlink" : '/.*' > /dev/null; then THIS="$softlink"; else THIS=`dirname "$THIS"`/"$softlink"; fi + done +# Detect the installation directory +INSTALLDIR=`dirname "$THIS"` + +if [ -x "$INSTALLDIR/jre/bin/java" ]; then + JAVA_CMD=$INSTALLDIR/jre/bin/java +else + # Use JAVA_HOME if it is set + if [ -z "$JAVA_HOME" ]; then JAVA_CMD=java; else JAVA_CMD=$JAVA_HOME/bin/java; fi +fi + +# Determine Flyway edition to use +POSITIONAL=() +while [[ $# -gt 0 ]]; do + POSITIONAL+=("$1") + flag="$1" + case $flag in + -community) + FLYWAY_EDITION=community + ;; + -pro) + FLYWAY_EDITION=pro + ;; + -enterprise) + FLYWAY_EDITION=enterprise + ;; + esac + shift +done +set -- "${POSITIONAL[@]}" +if [ -z "$FLYWAY_EDITION" ]; then + FLYWAY_EDITION=community +fi + +CP="$INSTALLDIR/lib/*:$INSTALLDIR/lib/$FLYWAY_EDITION/*:$INSTALLDIR/drivers/*" + +EXTRA_ARGS= +if $linux; then + EXTRA_ARGS=-Djava.security.egd=file:/dev/../dev/urandom +fi + +if `command -v cygpath > /dev/null`; then CP=`cygpath -pw "$CP"`; fi +"$JAVA_CMD" $JAVA_ARGS $EXTRA_ARGS -cp "$CP" org.flywaydb.commandline.Main "$@" + +# Exit using the same code returned from Java +exit $? \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/flyway-community.txt a/flyway-commandline/src/main/assembly/flyway-community.txt new file mode 100644 index 0000000..cec23e4 --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway-community.txt @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2010-2017 Boxfuse GmbH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git b/flyway-commandline/src/main/assembly/flyway-enterprise.txt a/flyway-commandline/src/main/assembly/flyway-enterprise.txt new file mode 100644 index 0000000..fdf2602 --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway-enterprise.txt @@ -0,0 +1,135 @@ +Flyway Enterprise Edition License and Support Agreement +======================================================= + +Any use of the Flyway Enterprise Edition software is subject to the terms of this license agreement (“Agreement”). Please read the full Agreement carefully. This License and Support Agreement relates to purchases of the Flyway Enterprise Edition Software made on or after 2nd August 2019. + +This Agreement sets out the conditions under which and the extent to which the end user (referred to as “Licensee” below) is granted the right to use the “Flyway Enterprise Edition” software (referred to as “Software” below) provided by : Boxfuse GmbH, Scwedenstr. 9/Gebäude 1, 13359 Berlin, Germany (referred to as “Licensor” below). + +The Licensee confirms that it accepts and agrees to be legally bound by all terms and conditions of this Agreement by downloading and/or installing and/or using the Software. If you do not accept these terms, do not download, install or use the Software. + +IMPORTANT NOTICES: +(i) A copy of Agreement will be sent with your Boxfuse GmbH invoice; +(ii) Where you sign a paper version or a bespoke version of this Agreement, that paper/bespoke version will take precedence over any subsequent Agreements that were sent with your invoice; and +(iii) This Agreement shall prevail over your standard terms and conditions (if any) attached to, enclosed with, or referred to in, any purchase order or confirmation of order. + +1. Grant of rights +------------------ + +1.1 Evaluation. Licensee is entitled to a 30 day free trial of the Software for the purposes of deciding whether or not the Software meets Licensee’s requirement. (“Evaluation Period”) During the Evaluation Period the following terms apply: + 1.1.1 During the Evaluation Period, Licensee hereby agrees that the Software is provided AS IS with no representation, guarantee or warranty of any kind as to its functionality, quality, performance, suitability or fitness for purpose. All other terms, conditions, representations and warranties expressed or implied whether by statute or otherwise are hereby expressly excluded. + 1.1.2 Licensor shall not be liable for any claim, damages or other liability arising from or in connection with Licensee’s use of the Software during the Evaluation Period. + 1.1.3 For the avoidance of doubt, during the Evaluation Period: (i) clauses 11.1, 10.3 and 14.2 of this Agreement shall not apply; and (ii) clause 14.1 shall apply except that reference to clause 14.2 is deleted. + 1.1.4 Before or upon expiry of the Evaluation Period: +(i) if, in Licensee’s sole opinion, the Software has met its requirements, and Licensee wishes to continue to use the Software beyond the end of the Evaluation Period, it can decide whether to obtain the equivalent Subscription Fee version. Once the appropriate Subscription has been obtained, this Agreement shall continue in force (except that this clause 1.1 shall no longer apply). +(ii) If Licensee decides that the Software does not meet Licensee’s requirements, or otherwise does not wish to enter into a paid up Subscription, then Licensee shall destroy the Software and all copies, in any form including partial copies or modifications of the Software received from the Licensor or made in connect with this Evaluation Period and all documentation relating thereto. Any rights of Licensee to use the Software shall cease. +1.2 Subscription. In consideration of Licensee paying to Licensor the Subscription Fee, Licensor grants Licensee a non-exclusive, non-transferable right to access and use the Software during the Subscription Period in accordance with this Agreement (“Subscription”). +1.3 The Subscription is personal to the Licensee. Licensee may not rent, lease, sub-license, sell, pledge, assign the benefit or delegate the burden of this Agreement or Software or hold this Agreement on trust for any other person. +1.4 Licensee is granted the following rights to the Software, but not to the source code of the Software: + 1.4.1 the right to temporarily reproduce, in whole or in parts, the Software, by any means and in any form, to the extent that uploading, displaying, running or other contractual use of the Software requires reproduction; and + 1.4.2 the right to permanently reproduce, in whole or in parts, the Software, to the extent that the creation of backups as are customary in the business requires reproduction. +1.5 Except as stated in this Agreement, Licensee has no right to use, incorporate into other products, copy, publish, display, modify or translate the Software or any modification, adaption or copy of the Software or any part thereof. Licensee may only decompile, reverse engineer, or disassemble the source code Software either in whole or in part, as expressly permitted under the Subscription or under Sections 50(A), (B) and (BA) of the Copyright, Designs and Patents Act 1988 (as amended or updated from time to time) or other applicable law. +1.6 The rights set out in this clause 1 may be exercised in any hardware and software environment of Licensee, for all currently known types of use (subject to any restrictions above); such rights are limited to the agreed multiple(s) of 10 production database schemas set out in the Licensor’s invoice. +1.7 Licensee shall not use the Software to manufacture or distribute a product that is substantially similar to or competitive with the Software. + +2. Ownership of intellectual property rights +-------------------------------------------- + +2.1 Licensee acknowledges that i) all Intellectual Property Rights in or relating to the Software are owned by or licensed to Licensor and ii) except as expressly granted under the Agreement, Licensee has no rights in the Software. +2.2 Licensee hereby agrees to refrain from any action which would diminish the Licensor’s Intellectual Property Rights in or relating to the Software or which would call those rights into question. +2.3 Licensee agrees not to delete, remove or alter any trade marks, logos, copyright notices or similar proprietary devices of the Licensor’s, including without limitation any electronic watermarks or other identifiers that may be incorporated in the Software. All representations of Licensors name or logo must remain as originally distributed. + +3. Remuneration +--------------- + +3.1 The Subscription Fee (together with any levies, duties and/or taxes imposed on Licensee in Licensee’s jurisdiction (including but not limited to, value added tax, sales tax, use tax and withholding tax)) shall be due and payable by Licensee when Licensor issues a license key to Licensee for the Software. The Subscription Fee means the fee payable by Licensee under this Agreement to Licensor (excluding VAT and all other relevant taxes, where applicable), as detailed by Licensor from time including through Licensor’s website or as part of a written quotation. +3.2 Licensee may not deduct any amounts from the Subscription Fee, unless otherwise specified in this Agreement. +3.3 Where Licensee have obtained the Software through a Reseller, the terms agreed with such Reseller in relation to invoicing and payment will apply instead of this clause 3. + +4. Confidentiality +------------------ + +4.1 Licensee agrees not to provide or disclose any confidential information of Licensor’s (including relating to or derived from the Software) to any third party, including where such confidential information is derived under any applicable law as set out in clause 1.5. +4.2 Other than the disclosures referred to in Licensor’s Privacy Policy, Licensor agrees not to provide or disclose to any third party any information of a confidential nature in any form whatsoever which is disclosed to Licensor by Licensee or on behalf of Licensee. +4.3 The provisions of clauses 4.1 and 4.2 will not apply to the extent that: + 4.3.1 such information is in the receiving party’s possession free from any restriction as to its use or disclosure; or + 4.3.2 the receiving party can demonstrate that such information is in the public domain (other than as a result of an unauthorised disclosure); or + 4.3.3 such information is required to be disclosed by law. +4.4 No information to which clause 4.3.3 applies shall be disclosed to a third party unless and until the receiving party has (unless prevented from doing so by law) (i) given the disclosing party reasonable written notice of such proposed disclosure, (ii) consulted with the disclosing party, and (iii) agreed with the disclosing party the content of the disclosure, provided that it shall not limit the disclosure in a manner which would prevent the receiving party from complying with a statutory or regulatory obligation or court order. + +5. Updates and upgrades +----------------------- + +5.1 The Subscription includes all updates and upgrades of the Software released during the Subscription Period without any further fees, unless otherwise agreed on in a separate agreement. + +6. Rights to the source code +---------------------------- + +6.1 The Licensee is granted the following rights to the source code of the Software: + 6.1.1 the right to edit the source code solely for the purpose of correcting defects, and under the condition that the Licensee shall transmit to the Licensor all modifications made and grant the Licensor all rights required for the commercial exploitation of these changes in new versions or follow-on products of the software, in accordance with the Contributor License Agreement (CLA) of the Licensor (https://cla-assistant.io/flyway/flyway), that is applicable at the time of submitting the modifications to the Licensor. + 6.1.2 If, while a valid Subscription for the Software is in effect, insolvency proceedings have been opened on the assets of the Licensor or the opening of such proceedings has been rejected due to lack of assets, the Licensee shall be entitled to edit the software more extensively for the purpose of adapting the software to changing software environments, thus ensuring its continued operability. + 6.1.3 The Licensee is not permitted to disclose the source code. + +7. Support +---------- + +7.1 The Subscription entitles the Licensee to technical support by e-mail for a period of one year, starting with the acquisition of the license. The Licensor agrees to provide the Licensee technical support at no charge. +7.2 Support requests must be sent by e-mail to support@flywaydb.org and shall be answered by email within 2 business days. + +8. Audit right +-------------- + +8.1 The Licensor has the right to verify the compliance of the Licensee with the license conditions. For this purpose, either the Licensor or a third party appointed by the Licensor shall be entitled, upon prior notice and with an appropriate lead time, to verify the compliant use of the software at the premises of the Licensee during normal business hours of the Licensee. The Licensee is obligated to assist the Licensor with the audit free of charge, and provide Licensor with adequate support, especially but not limited to granting access to information and to the systems on which the software is being used. + +9. Mention right +---------------- + +9.1 The Licensee grants the Licensor the right to mention the Licensee as a customer and user of the software, including the right to list the Licensee as a customer on Licensor’s website using both the Licensee’s company name and logo. + +10. Liability +------------- + +10.1 Nothing in this Agreement shall limit or exclude either party’s liability for: (a) personal injury or death resulting from negligence, (b) fraud; or (c) any other matter for which liability cannot be excluded by law. +10.2 Subject to clause 10.1, neither party shall be liable to the other party for any indirect, special or consequential loss or damage whatsoever arising under or in relation to this Agreement (whether in contract, tort (including negligence), breach of statutory duty, restitution or otherwise). The Licensor shall not be liable to the Licensee for any of the following types of loss or damage arising under or in relation to this Agreement: (a) any loss of profits, business, contracts, anticipated savings, goodwill, or revenue; or (b) any loss, or corruption, of software or data; or (c) any loss of use of hardware, software or data. +10.3 Subject to clauses 10.1, 10.2 and 14.2 the Licensor’s aggregate liability under and in connection with this Agreement howsoever caused shall be limited in all cases to the Subscription Fee. +10.4 The provisions of this clause 10 allocate risks under this Agreement between the Licensor and Licensee, and the Subscription Fee reflects this allocation of risks and these limitations of liability. + +11. Limited Warranty +-------------------- + +11.1 Licensor warrants that: + 11.1.1 Licensor owns the Intellectual Property Rights in the Software and/or have the right to grant a licence to Licensee; + 11.1.2 in creating the Software, Licensor has not knowingly infringed the intellectual property rights of third parties; and + 11.1.3 for a period of 90 days from the first installation of the Software (or, if applicable, 90 days from the end of the Evaluation Period if Licensee continues to use the Subscription Software) the Software shall operate substantially in accordance with its description. However, Licensee acknowledges that the Software is of such a complexity that there will be inherent defects and that therefore the Licensor can give no warranty that the Software is free from error or defect or that operation of the Software shall be uninterrupted. +11.2 Other than as provided for in clause 11.1 above, Licensor does not offer any warranty related to the Software provided, either express or implied, including but not limited to implied warranties of fitness for purpose or satisfactory quality. The Software has been developed as a standard product for use by a wide variety of users and so Licensor is unable to warrant that the Software will meet any particular user needs. Licensee shall take full responsibility for ensuring that the Software is suitable for Licensee’s intended purposes and to facilitate investigation into such suitability, Licensor offers a free Evaluation Period. + +12. Agreement Term +------------------ + +12.1 The Subscription is valid for a period of 12 months from the purchase by the Licensee of the Subscription (“Subscription Period”). The Subscription and this Agreement will terminate automatically at the end of the Subscription Period. +12.2 If Licensor does not receive the Subscription Fee, Licensor reserves the right to terminate the Subscription 30 days after the payment due date. +12.3 The Subscription will terminate automatically if Licensee uninstalls and ceases use of the Software, or uninstalls and destroys or voluntarily returns the Software to Licensor, and notifies Licensor that Licensee has done so. +12.4 Where the Subscription is terminated in accordance with clause 12.3 or clause 12.2, then the Agreement shall terminate in its entirety. +12.5 Upon termination of this Agreement: (a) Licensee must cease use of the Software, and uninstall, destroy or put beyond use all copies of the Software in Licensee’s possession or control, and (b) the provisions of clauses 1.7, 4, 10, 11.2, 12, 14 and 15.1 to 15.6 will remain in effect. + +13. Data collection and Privacy Policy +-------------------------------------- + +13.1 Information on the data Licensor collects about Licensee and how Licensor treats that data is set out in Licensor’s Privacy Policy, which can be viewed at: https://flywaydb.org/legal-information. + +14. Third party claims +---------------------- + +14.1 Licensee agrees to indemnify Licensor from any loss or damage whether in contract, tort (including negligence), breach of statutory duty, restitution or otherwise, if a third party claims that Licensee’s use of the Software causes any such loss or damage, except in the circumstances in clause 14.2 below. +14.2 If any claim is brought against Licensee alleging that their use of the intellectual property associated with the Software in accordance with this Agreement infringes the rights of any third party, Licensee shall promptly notify Licensor and supply full details of the claim. The parties shall consult together on an appropriate course of action and seek to minimise the effect of any claim on the respective businesses. Licensor shall have the right, but not the obligation, to take control of all negotiations and litigation arising out of the claim. Licensor will pay damages and costs awarded against Licensee in connection with any claim subject to a maximum of the Subscription Fee paid to Licensor by Licensee. Licensor shall have the right at its sole choice, to either; (i) use reasonable endeavours to negotiation terms for continued use by Licensee of the claimed infringing software; or (ii) use reasonable endeavours to amend the Software to make it non-infringing; or (iii) terminate this Agreement with immediate effect and in such event, Licensor shall refund to Licensee the Subscription Fee paid. + +15. General +----------- + +15.1 Governing law and settlement of disputes. This Agreement (and any dispute or claim relating to it, or its formation, existence, construction, performance, validity or termination) will be governed by and construed in accordance with the laws of England. The courts of England and Wales shall have non-exclusive jurisdiction to settle any dispute or claim arising out of or in connection with this agreement or its subject matter or formation (including non-contractual disputes or claims). Without prejudice to any other rights or remedies that Licensor may have, Licensee acknowledges and agrees that damages alone would not be an adequate remedy for any breach of clauses 1, 2 and 4 by Licensee. Accordingly, Licensor shall be entitled to seek an injunction or other equitable relief for any threatened or actual breach of those clauses. +15.2 Compliance with applicable law. Licensee agrees that, notwithstanding clause 15.1 above, Licensee may be subject to additional laws in other jurisdictions with respect to its use of the Software in such jurisdictions. Licensee agrees to comply with the laws of any such jurisdiction including, without limitation, any applicable export laws or regulations. +15.3 Severability. If any provision or part of any provision in this Agreement is found to be illegal, invalid or unenforceable for any reason then the remaining provisions or part provisions remain unaffected and the parties shall meet promptly to discuss in good faith and agree an alternative provision or part provision that provides as closely as possible, the same commercial effect as the original. +15.4 No waiver. No failure or delay by any party to exercise any right, power or remedy will operate as a waiver of it, nor will any partial exercise preclude any further exercise of the same, or of some other right, power or remedy. +15.5 No third party rights. Licensor and Licensee do not intend that any of this Agreement will be enforceable by virtue of the Contracts (Rights of Third Parties) Act 1999 by any person not a party to it and all rights by virtue of the Contracts (Rights of Third Parties) Act 1999 are hereby excluded. +15.6 Entire agreement. This Agreement contains all the terms which the parties have agreed in relation to the subject matter of this Agreement and supersedes any prior oral agreements, representations or understandings between the parties in relation to such subject matter. +15.7 Consumer regulations. This clause applies to Consumers only. Licensee shall have the right to cancel this Agreement 14 days from the date Licensee agrees to be obliged to pay for the Software under this Agreement. Should Licensee wish to cancel this Agreement under this clause 15.7, Licensee must notify Licensor of Licensee’s decision to cancel by sending Licensor an email clearly confirming its decision to: invoices@flywaydb.org. If Licensee has any complaints about this Agreement, including complaints about the Software, please raise these with Boxfuse GmbH using sales@flywaydb.org. + diff --git b/flyway-commandline/src/main/assembly/flyway-pro.txt a/flyway-commandline/src/main/assembly/flyway-pro.txt new file mode 100644 index 0000000..ba9642d --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway-pro.txt @@ -0,0 +1,133 @@ +Flyway Pro Edition License and Support Agreement +================================================ + +Any use of the Flyway Pro Edition software is subject to the terms of this license agreement (“Agreement”). Please read the full Agreement carefully. This License and Support Agreement relates to purchases of the Flyway Pro Edition Software made on or after 2nd August 2019. + +This Agreement sets out the conditions under which and the extent to which the end user (referred to as “Licensee” below) is granted the right to use the “Flyway Pro Edition” software (referred to as “Software” below) provided by : Boxfuse GmbH, Scwedenstr. 9/Gebäude 1, 13359 Berlin, Germany (referred to as “Licensor” below). + +The Licensee confirms that it accepts and agrees to be legally bound by all terms and conditions of this Agreement by downloading and/or installing and/or using the Software. If you do not accept these terms, do not download, install or use the Software. + +IMPORTANT NOTICES: +(i) A copy of Agreement will be sent with your Boxfuse GmbH invoice; +(ii) Where you sign a paper version or a bespoke version of this Agreement, that paper/bespoke version will take precedence over any subsequent Agreements that were sent with your invoice; and +(iii) This Agreement shall prevail over your standard terms and conditions (if any) attached to, enclosed with, or referred to in, any purchase order or confirmation of order. + +1. Grant of rights +------------------ + +1.1 Evaluation. Licensee is entitled to a 30 day free trial of the Software for the purposes of deciding whether or not the Software meets Licensee’s requirement. (“Evaluation Period”) During the Evaluation Period the following terms apply: + 1.1.1 During the Evaluation Period, Licensee hereby agrees that the Software is provided AS IS with no representation, guarantee or warranty of any kind as to its functionality, quality, performance, suitability or fitness for purpose. All other terms, conditions, representations and warranties expressed or implied whether by statute or otherwise are hereby expressly excluded. + 1.1.2 Licensor shall not be liable for any claim, damages or other liability arising from or in connection with Licensee’s use of the Software during the Evaluation Period. + 1.1.3 For the avoidance of doubt, during the Evaluation Period: (i) clauses 11.1, 10.3 and 14.2 of this Agreement shall not apply; and (ii) clause 14.1 shall apply except that reference to clause 14.2 is deleted. + 1.1.4 Before or upon expiry of the Evaluation Period: +(i) if, in Licensee’s sole opinion, the Software has met its requirements, and Licensee wishes to continue to use the Software beyond the end of the Evaluation Period, it can decide whether to obtain the equivalent Subscription Fee version. Once the appropriate Subscription has been obtained, this Agreement shall continue in force (except that this clause 1.1 shall no longer apply). +(ii) If Licensee decides that the Software does not meet Licensee’s requirements, or otherwise does not wish to enter into a paid up Subscription, then Licensee shall destroy the Software and all copies, in any form including partial copies or modifications of the Software received from the Licensor or made in connect with this Evaluation Period and all documentation relating thereto. Any rights of Licensee to use the Software shall cease. +1.2 Subscription. In consideration of Licensee paying to Licensor the Subscription Fee, Licensor grants Licensee a non-exclusive, non-transferable right to access and use the Software during the Subscription Period in accordance with this Agreement (“Subscription”). +1.3 The Subscription is personal to the Licensee. Licensee may not rent, lease, sub-license, sell, pledge, assign the benefit or delegate the burden of this Agreement or Software or hold this Agreement on trust for any other person. +1.4 Licensee is granted the following rights to the Software, but not to the source code of the Software: + 1.4.1 the right to temporarily reproduce, in whole or in parts, the Software, by any means and in any form, to the extent that uploading, displaying, running or other contractual use of the Software requires reproduction; and + 1.4.2 the right to permanently reproduce, in whole or in parts, the Software, to the extent that the creation of backups as are customary in the business requires reproduction. +1.5 Except as stated in this Agreement, Licensee has no right to use, incorporate into other products, copy, publish, display, modify or translate the Software or any modification, adaption or copy of the Software or any part thereof. Licensee may only decompile, reverse engineer, or disassemble the source code Software either in whole or in part, as expressly permitted under the Subscription or under Sections 50(A), (B) and (BA) of the Copyright, Designs and Patents Act 1988 (as amended or updated from time to time) or other applicable law. +1.6 The rights set out in this clause 1 may be exercised in any hardware and software environment of Licensee, for all currently known types of use (subject to any restrictions above); such rights are limited to the agreed multiple(s) of 10 production database schemas set out in the Licensor’s invoice. +1.7 Licensee shall not use the Software to manufacture or distribute a product that is substantially similar to or competitive with the Software. + +2. Ownership of intellectual property rights +-------------------------------------------- + +2.1 Licensee acknowledges that i) all Intellectual Property Rights in or relating to the Software are owned by or licensed to Licensor and ii) except as expressly granted under the Agreement, Licensee has no rights in the Software. +2.2 Licensee hereby agrees to refrain from any action which would diminish the Licensor’s Intellectual Property Rights in or relating to the Software or which would call those rights into question. +2.3 Licensee agrees not to delete, remove or alter any trade marks, logos, copyright notices or similar proprietary devices of the Licensor’s, including without limitation any electronic watermarks or other identifiers that may be incorporated in the Software. All representations of Licensors name or logo must remain as originally distributed. + +3. Remuneration +--------------- + +3.1 The Subscription Fee (together with any levies, duties and/or taxes imposed on Licensee in Licensee’s jurisdiction (including but not limited to, value added tax, sales tax, use tax and withholding tax)) shall be due and payable by Licensee when Licensor issues a license key to Licensee for the Software. The Subscription Fee means the fee payable by Licensee under this Agreement to Licensor (excluding VAT and all other relevant taxes, where applicable), as detailed by Licensor from time including through Licensor’s website or as part of a written quotation. +3.2 Licensee may not deduct any amounts from the Subscription Fee, unless otherwise specified in this Agreement. +3.3 Where Licensee have obtained the Software through a Reseller, the terms agreed with such Reseller in relation to invoicing and payment will apply instead of this clause 3. + +4. Confidentiality +------------------ + +4.1 Licensee agrees not to provide or disclose any confidential information of Licensor’s (including relating to or derived from the Software) to any third party, including where such confidential information is derived under any applicable law as set out in clause 1.5. +4.2 Other than the disclosures referred to in Licensor’s Privacy Policy, Licensor agrees not to provide or disclose to any third party any information of a confidential nature in any form whatsoever which is disclosed to Licensor by Licensee or on behalf of Licensee. +4.3 The provisions of clauses 4.1 and 4.2 will not apply to the extent that: + 4.3.1 such information is in the receiving party’s possession free from any restriction as to its use or disclosure; or + 4.3.2 the receiving party can demonstrate that such information is in the public domain (other than as a result of an unauthorised disclosure); or + 4.3.3 such information is required to be disclosed by law. +4.4 No information to which clause 4.3.3 applies shall be disclosed to a third party unless and until the receiving party has (unless prevented from doing so by law) (i) given the disclosing party reasonable written notice of such proposed disclosure, (ii) consulted with the disclosing party, and (iii) agreed with the disclosing party the content of the disclosure, provided that it shall not limit the disclosure in a manner which would prevent the receiving party from complying with a statutory or regulatory obligation or court order. + +5. Updates and upgrades +----------------------- + +5.1 The Subscription includes all updates and upgrades of the Software released during the Subscription Period without any further fees, unless otherwise agreed on in a separate agreement. + +6. Rights to the source code +---------------------------- + +6.1 The Licensee is granted the following rights to the source code of the Software: + 6.1.1 the right to edit the source code solely for the purpose of correcting defects, and under the condition that the Licensee shall transmit to the Licensor all modifications made and grant the Licensor all rights required for the commercial exploitation of these changes in new versions or follow-on products of the software, in accordance with the Contributor License Agreement (CLA) of the Licensor (https://cla-assistant.io/flyway/flyway), that is applicable at the time of submitting the modifications to the Licensor. + 6.1.2 If, while a valid Subscription for the Software is in effect, insolvency proceedings have been opened on the assets of the Licensor or the opening of such proceedings has been rejected due to lack of assets, the Licensee shall be entitled to edit the software more extensively for the purpose of adapting the software to changing software environments, thus ensuring its continued operability. + 6.1.3 The Licensee is not permitted to disclose the source code. + +7. Support +---------- + +7.1 The Subscription does not entitle the Licensee to technical support. + +8. Audit right +-------------- + +8.1 The Licensor has the right to verify the compliance of the Licensee with the license conditions. For this purpose, either the Licensor or a third party appointed by the Licensor shall be entitled, upon prior notice and with an appropriate lead time, to verify the compliant use of the software at the premises of the Licensee during normal business hours of the Licensee. The Licensee is obligated to assist the Licensor with the audit free of charge, and provide Licensor with adequate support, especially but not limited to granting access to information and to the systems on which the software is being used. + +9. Mention right +---------------- + +9.1 The Licensee grants the Licensor the right to mention the Licensee as a customer and user of the software, including the right to list the Licensee as a customer on Licensor’s website using both the Licensee’s company name and logo. + +10. Liability +------------- + +10.1 Nothing in this Agreement shall limit or exclude either party’s liability for: (a) personal injury or death resulting from negligence, (b) fraud; or (c) any other matter for which liability cannot be excluded by law. +10.2 Subject to clause 10.1, neither party shall be liable to the other party for any indirect, special or consequential loss or damage whatsoever arising under or in relation to this Agreement (whether in contract, tort (including negligence), breach of statutory duty, restitution or otherwise). The Licensor shall not be liable to the Licensee for any of the following types of loss or damage arising under or in relation to this Agreement: (a) any loss of profits, business, contracts, anticipated savings, goodwill, or revenue; or (b) any loss, or corruption, of software or data; or (c) any loss of use of hardware, software or data. +10.3 Subject to clauses 10.1, 10.2 and 14.2 the Licensor’s aggregate liability under and in connection with this Agreement howsoever caused shall be limited in all cases to the Subscription Fee. +10.4 The provisions of this clause 10 allocate risks under this Agreement between the Licensor and Licensee, and the Subscription Fee reflects this allocation of risks and these limitations of liability. + +11. Limited Warranty +-------------------- + +11.1 Licensor warrants that: + 11.1.1 Licensor owns the Intellectual Property Rights in the Software and/or have the right to grant a licence to Licensee; + 11.1.2 in creating the Software, Licensor has not knowingly infringed the intellectual property rights of third parties; and + 11.1.3 for a period of 90 days from the first installation of the Software (or, if applicable, 90 days from the end of the Evaluation Period if Licensee continues to use the Subscription Software) the Software shall operate substantially in accordance with its description. However, Licensee acknowledges that the Software is of such a complexity that there will be inherent defects and that therefore the Licensor can give no warranty that the Software is free from error or defect or that operation of the Software shall be uninterrupted. +11.2 Other than as provided for in clause 11.1 above, Licensor does not offer any warranty related to the Software provided, either express or implied, including but not limited to implied warranties of fitness for purpose or satisfactory quality. The Software has been developed as a standard product for use by a wide variety of users and so Licensor is unable to warrant that the Software will meet any particular user needs. Licensee shall take full responsibility for ensuring that the Software is suitable for Licensee’s intended purposes and to facilitate investigation into such suitability, Licensor offers a free Evaluation Period. + +12. Agreement Term +------------------ + +12.1 The Subscription is valid for a period of 12 months from the purchase by the Licensee of the Subscription (“Subscription Period”). The Subscription and this Agreement will terminate automatically at the end of the Subscription Period. +12.2 If Licensor does not receive the Subscription Fee, Licensor reserves the right to terminate the Subscription 30 days after the payment due date. +12.3 The Subscription will terminate automatically if Licensee uninstalls and ceases use of the Software, or uninstalls and destroys or voluntarily returns the Software to Licensor, and notifies Licensor that Licensee has done so. +12.4 Where the Subscription is terminated in accordance with clause 12.3 or clause 12.2, then the Agreement shall terminate in its entirety. +12.5 Upon termination of this Agreement: (a) Licensee must cease use of the Software, and uninstall, destroy or put beyond use all copies of the Software in Licensee’s possession or control, and (b) the provisions of clauses 1.7, 4, 10, 11.2, 12,14 and 15.1 to 15.6 will remain in effect. + +13. Data collection and Privacy Policy +-------------------------------------- + +13.1 Information on the data Licensor collects about Licensee and how Licensor treats that data is set out in Licensor’s Privacy Policy, which can be viewed at: https://flywaydb.org/legal-information. + +14. Third party claims +---------------------- + +14.1 Licensee agrees to indemnify Licensor from any loss or damage whether in contract, tort (including negligence), breach of statutory duty, restitution or otherwise, if a third party claims that Licensee’s use of the Software causes any such loss or damage, except in the circumstances in clause 14.2 below. +14.2 If any claim is brought against Licensee alleging that their use of the intellectual property associated with the Software in accordance with this Agreement infringes the rights of any third party, Licensee shall promptly notify Licensor and supply full details of the claim. The parties shall consult together on an appropriate course of action and seek to minimise the effect of any claim on the respective businesses. Licensor shall have the right, but not the obligation, to take control of all negotiations and litigation arising out of the claim. Licensor will pay damages and costs awarded against Licensee in connection with any claim subject to a maximum of the Subscription Fee paid to Licensor by Licensee. Licensor shall have the right at its sole choice, to either; (i) use reasonable endeavours to negotiation terms for continued use by Licensee of the claimed infringing software; or (ii) use reasonable endeavours to amend the Software to make it non-infringing; or (iii) terminate this Agreement with immediate effect and in such event, Licensor shall refund to Licensee the Subscription Fee paid. + +15. General +----------- + +15.1 Governing law and settlement of disputes. This Agreement (and any dispute or claim relating to it, or its formation, existence, construction, performance, validity or termination) will be governed by and construed in accordance with the laws of England. The courts of England and Wales shall have non-exclusive jurisdiction to settle any dispute or claim arising out of or in connection with this agreement or its subject matter or formation (including non-contractual disputes or claims). Without prejudice to any other rights or remedies that Licensor may have, Licensee acknowledges and agrees that damages alone would not be an adequate remedy for any breach of clauses 1, 2 and 4 by Licensee. Accordingly, Licensor shall be entitled to seek an injunction or other equitable relief for any threatened or actual breach of those clauses. +15.2 Compliance with applicable law. Licensee agrees that, notwithstanding clause 15.1 above, Licensee may be subject to additional laws in other jurisdictions with respect to its use of the Software in such jurisdictions. Licensee agrees to comply with the laws of any such jurisdiction including, without limitation, any applicable export laws or regulations. +15.3 Severability. If any provision or part of any provision in this Agreement is found to be illegal, invalid or unenforceable for any reason then the remaining provisions or part provisions remain unaffected and the parties shall meet promptly to discuss in good faith and agree an alternative provision or part provision that provides as closely as possible, the same commercial effect as the original. +15.4 No waiver. No failure or delay by any party to exercise any right, power or remedy will operate as a waiver of it, nor will any partial exercise preclude any further exercise of the same, or of some other right, power or remedy. +15.5 No third party rights. Licensor and Licensee do not intend that any of this Agreement will be enforceable by virtue of the Contracts (Rights of Third Parties) Act 1999 by any person not a party to it and all rights by virtue of the Contracts (Rights of Third Parties) Act 1999 are hereby excluded. +15.6 Entire agreement. This Agreement contains all the terms which the parties have agreed in relation to the subject matter of this Agreement and supersedes any prior oral agreements, representations or understandings between the parties in relation to such subject matter. +15.7 Consumer regulations. This clause applies to Consumers only. Licensee shall have the right to cancel this Agreement 14 days from the date Licensee agrees to be obliged to pay for the Software under this Agreement. Should Licensee wish to cancel this Agreement under this clause 15.7, Licensee must notify Licensor of Licensee’s decision to cancel by sending Licensor an email clearly confirming its decision to: invoices@flywaydb.org. If Licensee has any complaints about this Agreement, including complaints about the Software, please raise these with Boxfuse GmbH using sales@flywaydb.org. diff --git b/flyway-commandline/src/main/assembly/flyway.cmd a/flyway-commandline/src/main/assembly/flyway.cmd new file mode 100644 index 0000000..b9ae897 --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway.cmd @@ -0,0 +1,71 @@ +@REM +@REM Copyright 2010-2020 Redgate Software Ltd +@REM +@REM Licensed under the Apache License, Version 2.0 (the "License"); +@REM you may not use this file except in compliance with the License. +@REM You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, software +@REM distributed under the License is distributed on an "AS IS" BASIS, +@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@REM See the License for the specific language governing permissions and +@REM limitations under the License. +@REM + +@Echo off + +setlocal + +@REM Set the current directory to the installation directory +call :getCurrentBatch INSTALLDIR1 +set INSTALLDIR=%INSTALLDIR1% +set INSTALLDIR=%INSTALLDIR:~0,-10% + +if exist "%INSTALLDIR%\jre\bin\java.exe" ( + set JAVA_CMD="%INSTALLDIR%\jre\bin\java.exe" +) else ( + @REM Use JAVA_HOME if it is set + if "%JAVA_HOME%"=="" ( + set JAVA_CMD=java + ) else ( + set JAVA_CMD="%JAVA_HOME%\bin\java.exe" + ) +) + +if "%JAVA_ARGS%"=="" ( + set JAVA_ARGS= +) + +@REM Determine Flyway edition to use +:loop +IF NOT [%1]==[] ( + IF [%1]==[-community] ( + SET FLYWAY_EDITION=community + GOTO :loop-end + ) + IF [%1]==[-pro] ( + SET FLYWAY_EDITION=pro + GOTO :loop-end + ) + IF [%1]==[-enterprise] ( + SET FLYWAY_EDITION=enterprise + GOTO :loop-end + ) + SHIFT /1 + GOTO :loop +) +:loop-end +if "%FLYWAY_EDITION%"=="" ( + set FLYWAY_EDITION=community +) + +%JAVA_CMD% %JAVA_ARGS% -cp "%CLASSPATH%;%INSTALLDIR%\lib\*;%INSTALLDIR%\lib\%FLYWAY_EDITION%\*;%INSTALLDIR%\drivers\*" org.flywaydb.commandline.Main %* + +@REM Exit using the same code returned from Java +EXIT /B %ERRORLEVEL% + +:getCurrentBatch + set "%~1=%~f0" + goto :eof \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/flyway.conf a/flyway-commandline/src/main/assembly/flyway.conf new file mode 100644 index 0000000..7006a40 --- /dev/null +++ a/flyway-commandline/src/main/assembly/flyway.conf @@ -0,0 +1,343 @@ +# +# Copyright 2010-2020 Boxfuse GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# JDBC url to use to connect to the database +# Examples +# -------- +# Most drivers are included out of the box. +# * = JDBC driver must be downloaded and installed in /drivers manually +# ** = TNS_ADMIN environment variable must point to the directory of where tnsnames.ora resides +# Aurora MySQL : jdbc:mysql://..rds.amazonaws.com:/?=&=... +# Aurora PostgreSQL : jdbc:postgresql://..rds.amazonaws.com:/?=&=... +# CockroachDB : jdbc:postgresql://:/?=&=... +# DB2* : jdbc:db2://:/ +# Derby : jdbc:derby::<;attribute=value> +# Firebird : jdbc:firebirdsql://[:]/?=&=... +# H2 : jdbc:h2: +# HSQLDB : jdbc:hsqldb:file: +# Informix* : jdbc:informix-sqli://:/:informixserver=dev +# MariaDB : jdbc:mariadb://:/?=&=... +# MySQL : jdbc:mysql://:/?=&=... +# Oracle : jdbc:oracle:thin:@//:/ +# Oracle (TNS)** : jdbc:oracle:thin:@ +# PostgreSQL : jdbc:postgresql://:/?=&=... +# SAP HANA* : jdbc:sap://:/?databaseName= +# Snowflake* : jdbc:snowflake://.snowflakecomputing.com/?db=&warehouse=&role=... +# SQL Server : jdbc:sqlserver://:;databaseName= +# SQLite : jdbc:sqlite: +# Sybase ASE : jdbc:jtds:sybase://:/ +# Redshift* : jdbc:redshift://:/ +# flyway.url= + +# Fully qualified classname of the JDBC driver (autodetected by default based on flyway.url) +# flyway.driver= + +# User to use to connect to the database. Flyway will prompt you to enter it if not specified, and if the JDBC +# connection is not using a password-less method of authentication. +# flyway.user= + +# Password to use to connect to the database. Flyway will prompt you to enter it if not specified, and if the JDBC +# connection is not using a password-less method of authentication. +# flyway.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. (default: 0) +# flyway.connectRetries= + +# The SQL statements to run to initialize a new database connection immediately after opening it. (default: none) +# flyway.initSql= + +# The default schema managed by Flyway. This schema name is case-sensitive. If not specified, but flyway.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). +# flyway.defaultSchema= + +# Comma-separated list of schemas managed by Flyway. These schema names are case-sensitive. If not specified, Flyway uses +# the default schema for the database connection. If flyway.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. +# flyway.schemas= + +# Whether Flyway should attempt to create the schemas specified in the schemas property +# flyway.createSchemas= + +# Name of Flyway's schema history table (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. +# flyway.table= + +# 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. +# flyway.tablespace= + +# Comma-separated list of locations to scan recursively for migrations. (default: filesystem:<>/sql) +# The location type is determined by its prefix. +# Unprefixed locations or locations starting with classpath: point to a package on the classpath and may contain +# both SQL and Java-based migrations. +# Locations starting with filesystem: point to a directory on the filesystem, may only +# contain SQL migrations and are only scanned recursively down non-hidden directories. +# Wildcards can be used to reduce duplication of location paths. (e.g. filesystem:migrations/*/oracle) Supported wildcards: +# ** : Matches any 0 or more directories +# * : Matches any 0 or more non-separator characters +# ? : Matches any 1 non-separator character +# flyway.locations= + +# Comma-separated list of fully qualified class names of custom MigrationResolver to use for resolving migrations. +# flyway.resolvers= + +# If set to true, default built-in resolvers (jdbc, spring-jdbc and sql) are skipped and only custom resolvers as +# defined by 'flyway.resolvers' are used. (default: false) +# flyway.skipDefaultResolvers= + +# Comma-separated list of directories containing JDBC drivers and Java-based migrations. +# (default: /jars) +# flyway.jarDirs= + +# 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 +# flyway.sqlMigrationPrefix= + +# 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 +# flyway.undoSqlMigrationPrefix= + +# 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 +# flyway.repeatableSqlMigrationPrefix= + +# 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 +# flyway.sqlMigrationSeparator= + +# Comma-separated list of 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. +# flyway.sqlMigrationSuffixes= + +# Whether to stream SQL migrations when executing them. (default: false) +# 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 +# flyway.stream= + +# Whether to batch SQL statements when executing them. (default: false) +# 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 +# flyway.batch= + +# Encoding of SQL migrations (default: UTF-8). Caution: changing the encoding after migrations have been run +# will invalidate the calculated checksums and require a `flyway repair`. +# flyway.encoding= + +# Whether placeholders should be replaced. (default: true) +# flyway.placeholderReplacement= + +# Placeholders to replace in Sql migrations +# flyway.placeholders.user= +# flyway.placeholders.my_other_placeholder= + +# Prefix of every placeholder (default: ${ ) +# flyway.placeholderPrefix= + +# Suffix of every placeholder (default: } ) +# flyway.placeholderSuffix= + +# Target version up to which Flyway should consider migrations. +# Defaults to 'latest' +# Special values: +# - 'current': designates the current version of the schema +# - 'latest': the latest version of the schema, as defined by the migration with the highest version +# flyway.target= + +# Whether to automatically call validate or not when running migrate. (default: true) +# flyway.validateOnMigrate= + +# Whether to automatically call clean or not when a validation error occurs. (default: 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 ! +# flyway.cleanOnValidationError= + +# Whether to disable clean. (default: false) +# This is especially useful for production environments where running clean can be quite a career limiting move. +# flyway.cleanDisabled= + +# The version to tag an existing schema with when executing baseline. (default: 1) +# flyway.baselineVersion= + +# The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) +# flyway.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 initialized with the baselineVersion before executing the migrations. +# Only migrations above 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: false) +# flyway.baselineOnMigrate= + +# Allows migrations to be run "out of order" (default: false). +# 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. +# flyway.outOfOrder= + +# Whether Flyway should output a table with the results of queries when executing migrations (default: true). +# Flyway Pro and Flyway Enterprise only +# flyway.outputQueryResults= + +# This allows you to tie in custom code and logic to the Flyway lifecycle notifications (default: empty). +# Set this to a comma-separated list of fully qualified class names of org.flywaydb.core.api.callback.Callback +# implementations. +# flyway.callbacks= + +# If set to true, default built-in callbacks (sql) are skipped and only custom callback as +# defined by 'flyway.callbacks' are used. (default: false) +# flyway.skipDefaultCallbacks= + +# 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. +# true to continue normally and log a warning, false to fail fast with an exception. (default: false) +# flyway.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. +# true to continue normally, false to fail fast with an exception. (default: false) +# flyway.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. +# (default: false) +# flyway.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. +# true to continue normally and log a warning, false to fail fast with an exception. (default: true) +# flyway.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. +# false to continue normally, true to fail fast with an exception (default: false) +# flyway.validateMigrationNaming= + +# Whether to allow mixing transactional and non-transactional statements within the same migration. +# Flyway attempts to run each migration within its own transaction +# If Flyway detects that a specific statement cannot be run within a transaction, it won’t run that migration within a transaction +# This option toggles whether transactional and non-transactional statements can be mixed within a migration run. +# Enabling this means for 'mixed' migrations, the entire script will 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 within a transaction, the database will issue an implicit commit before and after +# its execution. +# true if mixed migrations should be allowed. false if an error should be thrown instead. (default: false) +# flyway.mixed= + +# Whether to group all pending migrations together in the same transaction when applying them +# (only recommended for databases with support for DDL transactions). +# true if migrations should be grouped. false if they should be applied individually instead. (default: false) +# flyway.group= + +# The username that will be recorded in the schema history table as having applied the migration. +# <> for the current database user of the connection. (default: <>). +# flyway.installedBy= + +# 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: 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: +# - D to force a debug message +# - D- to force a debug message, but do not show the original sql state and error code +# - I to force an info message +# - I- to force an info message, but do not show the original sql state and error code +# - W to force a warning +# - W- to force a warning, but do not show the original sql state and error code +# - E to force an error +# - 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: 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: 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: *:123:W +# Flyway Pro and Flyway Enterprise only +# flyway.errorOverrides= + +# The file where to output the SQL statements of a migration dry run. If the file specified is in a non-existent +# directory, Flyway will create all directories and parent directories as needed. +# <> to execute the SQL statements directly against the database. (default: <>) +# Flyway Pro and Flyway Enterprise only +# flyway.dryRunOutput= + +# Whether to Flyway's support for Oracle SQL*Plus commands should be activated. (default: false) +# Flyway Pro and Flyway Enterprise only +# flyway.oracle.sqlplus= + +# Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus +# statement it doesn't yet support. (default: false) +# Flyway Pro and Flyway Enterprise only +# flyway.oracle.sqlplusWarn= + +# Your Flyway license key (FL01...). Not yet a Flyway Pro or Enterprise Edition customer? +# Request your Flyway trial license key st https://flywaydb.org/download/ +# to try out Flyway Pro and Enterprise Edition features free for 30 days. +# Flyway Pro and Flyway Enterprise only +# flyway.licenseKey= \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/linux.xml a/flyway-commandline/src/main/assembly/linux.xml new file mode 100644 index 0000000..d264a84 --- /dev/null +++ a/flyway-commandline/src/main/assembly/linux.xml @@ -0,0 +1,36 @@ + + + linux-x64 + + tar.gz + + flyway-${project.version} + + src/main/assembly/component.xml + + + + target/dependency/jre-linux-x64-tar.gz/jdk-${version.jre}+9-jre + jre + 755 + + + \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/macos.xml a/flyway-commandline/src/main/assembly/macos.xml new file mode 100644 index 0000000..3327e22 --- /dev/null +++ a/flyway-commandline/src/main/assembly/macos.xml @@ -0,0 +1,36 @@ + + + macosx-x64 + + tar.gz + + flyway-${project.version} + + src/main/assembly/component.xml + + + + target/dependency/jre-macos-x64-tar.gz/jdk-${version.jre}+9-jre/Contents/Home + jre + 755 + + + \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/no-jre.xml a/flyway-commandline/src/main/assembly/no-jre.xml new file mode 100644 index 0000000..7a4b500 --- /dev/null +++ a/flyway-commandline/src/main/assembly/no-jre.xml @@ -0,0 +1,30 @@ + + + no-jre + + tar.gz + zip + + flyway-${project.version} + + src/main/assembly/component.xml + + \ No newline at end of file diff --git b/flyway-commandline/src/main/assembly/put-your-java-migration-jars-here.txt a/flyway-commandline/src/main/assembly/put-your-java-migration-jars-here.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ a/flyway-commandline/src/main/assembly/put-your-java-migration-jars-here.txt diff --git b/flyway-commandline/src/main/assembly/put-your-jdbc-drivers-here.txt a/flyway-commandline/src/main/assembly/put-your-jdbc-drivers-here.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ a/flyway-commandline/src/main/assembly/put-your-jdbc-drivers-here.txt diff --git b/flyway-commandline/src/main/assembly/put-your-sql-migrations-here.txt a/flyway-commandline/src/main/assembly/put-your-sql-migrations-here.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ a/flyway-commandline/src/main/assembly/put-your-sql-migrations-here.txt diff --git b/flyway-commandline/src/main/assembly/windows.xml a/flyway-commandline/src/main/assembly/windows.xml new file mode 100644 index 0000000..18c6f38 --- /dev/null +++ a/flyway-commandline/src/main/assembly/windows.xml @@ -0,0 +1,36 @@ + + + windows-x64 + + zip + + flyway-${project.version} + + src/main/assembly/component.xml + + + + target/dependency/jre-windows-x64-zip/jdk-${version.jre}+9-jre + jre + 644 + + + \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/ColorizedConsoleLog.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/ColorizedConsoleLog.java new file mode 100644 index 0000000..f0f345c --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/ColorizedConsoleLog.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.commandline; + +import org.flywaydb.core.api.logging.Log; +import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.Ansi.Color; +import org.fusesource.jansi.AnsiConsole; + +import java.io.PrintStream; + +/** + * Wrapper which adds color to a Console output. + */ +class ColorizedConsoleLog implements Log { + private final ConsoleLog log; + + public static void install(boolean force) { + if (force) { + System.setProperty("jansi.force", "true"); + } + + AnsiConsole.systemInstall(); + } + + public ColorizedConsoleLog(ConsoleLog log) { + this.log = log; + } + + @Override + public boolean isDebugEnabled() { + return this.log.isDebugEnabled(); + } + + @Override + public void debug(String message) { + colorizeBright(System.out, Color.BLACK); + this.log.debug(message); + reset(System.out); + } + + @Override + public void info(String message) { + if (message.startsWith("Successfully")) { + colorize(System.out, Color.GREEN); + this.log.info(message); + reset(System.out); + } else { + this.log.info(message); + } + } + + @Override + public void warn(String message) { + colorize(System.out, Color.YELLOW); + this.log.warn(message); + reset(System.out); + } + + @Override + public void error(String message) { + colorize(System.err, Color.RED); + this.log.error(message); + reset(System.err); + } + + @Override + public void error(String message, Exception e) { + colorize(System.err, Color.RED); + this.log.error(message, e); + reset(System.err); + } + + private void colorize(PrintStream stream, Color color) { + stream.print(Ansi.ansi().fg(color)); + } + + private void colorizeBright(PrintStream stream, Color color) { + stream.print(Ansi.ansi().fgBright(color)); + } + + private void reset(PrintStream stream) { + stream.print(Ansi.ansi().reset()); + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/CommandLineArguments.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/CommandLineArguments.java new file mode 100644 index 0000000..f66090f --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/CommandLineArguments.java @@ -0,0 +1,288 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.commandline; + +import org.flywaydb.commandline.ConsoleLog.Level; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.internal.util.StringUtils; + +import java.util.*; + +class CommandLineArguments { + + enum Color { + ALWAYS("always"), + NEVER("never"), + AUTO("auto"); + + private final String value; + + Color(String value) { + this.value = value; + } + + public static Color fromString(String value) { + if (value.isEmpty()) { + return AUTO; + } + + for (Color color : values()) { + if (color.value.equals(value)) { + return color; + } + } + + return null; + } + + public static boolean isValid(String value) { + return fromString(value) != null; + } + } + + // Flags + private static String DEBUG_FLAG = "-X"; + private static String QUIET_FLAG = "-q"; + private static String SUPPRESS_PROMPT_FLAG = "-n"; + private static String PRINT_VERSION_AND_EXIT_FLAG = "-v"; + private static String JSON_FLAG = "-json"; + private static String PRINT_USAGE_FLAG = "-?"; + private static String COMMUNITY_FLAG = "-community"; + private static String PRO_FLAG = "-pro"; + private static String ENTERPRISE_FLAG = "-enterprise"; + + // Command line specific configuration options + private static String OUTPUT_FILE = "outputFile"; + private static String LOG_FILE = "logFile"; + private static String CONFIG_FILE_ENCODING = "configFileEncoding"; + private static String CONFIG_FILES = "configFiles"; + private static String COLOR = "color"; + private static String WORKING_DIRECTORY = "workingDirectory"; + + private static List VALID_OPERATIONS_AND_FLAGS = Arrays.asList( + DEBUG_FLAG, + QUIET_FLAG, + SUPPRESS_PROMPT_FLAG, + PRINT_VERSION_AND_EXIT_FLAG, + JSON_FLAG, + PRINT_USAGE_FLAG, + COMMUNITY_FLAG, + PRO_FLAG, + ENTERPRISE_FLAG, + "help", + "migrate", + "clean", + "info", + "validate", + "undo", + "baseline", + "repair" + ); + + private final String[] args; + + CommandLineArguments(String[] args) { + this.args = args; + } + + private static boolean isFlagSet(String[] args, String flag) { + for (String arg : args) { + if (flag.equals(arg)) { + return true; + } + } + return false; + } + + private static String getArgumentValue(String argName, String[] allArgs) { + for (String arg : allArgs) { + if (arg.startsWith("-" + argName + "=")) { + return parseConfigurationOptionValueFromArg(arg); + } + } + return ""; + } + + private static String parseConfigurationOptionValueFromArg(String arg) { + int index = arg.indexOf("="); + + if (index < 0 || index == arg.length()) { + return ""; + } + + return arg.substring(index + 1); + } + + private static List getOperationsFromArgs(String[] args) { + List operations = new ArrayList<>(); + + for (String arg : args) { + if (!arg.startsWith("-")) { + operations.add(arg); + } + } + return operations; + } + + private static List getConfigFilesFromArgs(String[] args) { + String configFilesCommaSeparatedList = getArgumentValue(CONFIG_FILES, args); + + return Arrays.asList(StringUtils.tokenizeToStringArray(configFilesCommaSeparatedList, ",")); + } + + private static Map getConfigurationFromArgs(String[] args) { + Map configuration = new HashMap<>(); + + for (String arg : args) { + if (isConfigurationArg(arg)) { + String configurationOptionName = getConfigurationOptionNameFromArg(arg); + + if (!isConfigurationOptionIgnored(configurationOptionName)) { + configuration.put("flyway." + configurationOptionName, parseConfigurationOptionValueFromArg(arg)); + } + } + } + + return configuration; + } + + private static boolean isConfigurationOptionIgnored(String configurationOptionName) { + return OUTPUT_FILE.equals(configurationOptionName) || + LOG_FILE.equals(configurationOptionName) || + COLOR.equals(configurationOptionName) || + WORKING_DIRECTORY.equals(configurationOptionName); + } + + private static String getConfigurationOptionNameFromArg(String arg) { + int index = arg.indexOf("="); + + return arg.substring(1, index); + } + + private static boolean isConfigurationArg(String arg) { + return arg.startsWith("-") && arg.contains("="); + } + + void validate(Log log) { + for (String arg : args) { + if (!isConfigurationArg(arg) && !CommandLineArguments.VALID_OPERATIONS_AND_FLAGS.contains(arg)) { + throw new FlywayException("Invalid argument: " + arg); + } + } + + if (isLogFilepathSet()) { + if (isOutputFileSet()) { + throw new FlywayException("-logFile and -outputFile are incompatible. -logFile is deprecated. Instead use -outputFile."); + } + + if (shouldOutputJson()) { + throw new FlywayException("-logFile and -json are incompatible. -logFile is deprecated. Instead use -outputFile to print JSON to a file."); + } + + log.warn("-logFile is deprecated. Instead use -outputFile."); + } + + if (shouldOutputJson() && !hasOperation("info") ) { + throw new FlywayException("The -json flag is only supported by the info command."); + } + + String colorArgumentValue = getArgumentValue(COLOR, args); + if (!Color.isValid(colorArgumentValue)) { + throw new FlywayException("'" + colorArgumentValue + "' is an invalid value for the -color option. Use 'always', 'never', or 'auto'."); + } + } + + boolean shouldSuppressPrompt() { + return isFlagSet(args, SUPPRESS_PROMPT_FLAG); + } + + boolean shouldPrintVersionAndExit() { + return isFlagSet(args, PRINT_VERSION_AND_EXIT_FLAG); + } + + boolean shouldOutputJson() { + return isFlagSet(args, JSON_FLAG); + } + + boolean shouldPrintUsage() { + return isFlagSet(args, PRINT_USAGE_FLAG) || getOperations().isEmpty(); + } + + Level getLogLevel() { + if (isFlagSet(args, QUIET_FLAG)) { + return Level.WARN; + } + + if (isFlagSet(args, DEBUG_FLAG)) { + return Level.DEBUG; + } + + return Level.INFO; + } + + boolean hasOperation(String operation) { + return getOperations().contains(operation); + } + + List getOperations() { + return getOperationsFromArgs(args); + } + + List getConfigFiles() { + return getConfigFilesFromArgs(args); + } + + String getOutputFile() { + return getArgumentValue(OUTPUT_FILE, args); + } + + String getLogFilepath() { + return getArgumentValue(LOG_FILE, args); + } + + String getWorkingDirectory() { + return getArgumentValue(WORKING_DIRECTORY, args); + } + + boolean isOutputFileSet() { + return !getOutputFile().isEmpty(); + } + + boolean isLogFilepathSet() { + return !getLogFilepath().isEmpty(); + } + + boolean isWorkingDirectorySet() { + return !getWorkingDirectory().isEmpty(); + } + + String getConfigFileEncoding() { + return getArgumentValue(CONFIG_FILE_ENCODING, args); + } + + boolean isConfigFileEncodingSet() { + return !getConfigFileEncoding().isEmpty(); + } + + Color getColor() { + return Color.fromString(getArgumentValue(COLOR, args)); + } + + Map getConfiguration() { + return getConfigurationFromArgs(args); + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLog.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLog.java new file mode 100644 index 0000000..dcec32f --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLog.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.commandline; + +import org.flywaydb.core.api.logging.Log; + +/** + * Console output for standard output and standard error. + */ +class ConsoleLog implements Log { + public enum Level { + DEBUG, INFO, WARN + } + + private final Level level; + + /** + * Creates a new PrintStream Log. + * + * @param level the log level. + */ + public ConsoleLog(Level level) { + this.level = level; + } + + @Override + public boolean isDebugEnabled() { + return level == Level.DEBUG; + } + + public void debug(String message) { + if (isDebugEnabled()) { + System.out.println("DEBUG: " + message); + } + } + + public void info(String message) { + if (level.compareTo(Level.INFO) <= 0) { + System.out.println(message); + } + } + + public void warn(String message) { + System.out.println("WARNING: " + message); + } + + public void error(String message) { + System.err.println("ERROR: " + message); + } + + public void error(String message, Exception e) { + System.err.println("ERROR: " + message); + e.printStackTrace(System.err); + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLogCreator.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLogCreator.java new file mode 100644 index 0000000..ced631d --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/ConsoleLogCreator.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.commandline; + +import org.flywaydb.commandline.CommandLineArguments.Color; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; + +/** + * Log Creator for the Command-Line console. + */ +class ConsoleLogCreator implements LogCreator { + private final CommandLineArguments commandLineArguments; + + /** + * Creates a new Console Log Creator. + * + * @param commandLineArguments The command line arguments. + */ + ConsoleLogCreator(CommandLineArguments commandLineArguments) { + this.commandLineArguments = commandLineArguments; + } + + public Log createLogger(Class clazz) { + ConsoleLog log = new ConsoleLog(commandLineArguments.getLogLevel()); + Color color = commandLineArguments.getColor(); + + if (Color.NEVER.equals(color) || (Color.AUTO.equals(color) && System.console() == null)) { + return log; + } + + ColorizedConsoleLog.install(Color.ALWAYS.equals(color)); + return new ColorizedConsoleLog(log); + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLog.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLog.java new file mode 100644 index 0000000..8decc30 --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLog.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.commandline; + +import org.flywaydb.commandline.ConsoleLog.Level; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +class FileLog implements Log { + + private final Path path; + private final Level level; + + public FileLog(Path path, Level level) { + this.path = path; + this.level = level; + } + + @Override + public boolean isDebugEnabled() { + return level == Level.DEBUG; + } + + @Override + public void debug(String message) { + if (isDebugEnabled()) { + writeLogMessage("DEBUG", message); + } + } + + @Override + public void info(String message) { + if (level.compareTo(Level.INFO) <= 0) { + writeLogMessage(message); + } + } + + @Override + public void warn(String message) { + writeLogMessage("WARNING", message); + } + + @Override + public void error(String message) { + writeLogMessage("ERROR", message); + } + + @Override + public void error(String message, Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + String stackTrace = stringWriter.toString(); + + writeLogMessage("ERROR", message); + writeLogMessage(stackTrace); + } + + private void writeLogMessage(String prefix, String message) { + String logMessage = prefix + ": " + message; + writeLogMessage(logMessage); + } + + private void writeLogMessage(String logMessage) { + try { + Files.write(path, (logMessage + "\n").getBytes(), StandardOpenOption.APPEND, StandardOpenOption.WRITE); + } catch(IOException exception) { + throw new FlywayException("Could not write to file at " + path + ".", exception); + } + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLogCreator.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLogCreator.java new file mode 100644 index 0000000..2434c49 --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/FileLogCreator.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.commandline; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.flywaydb.commandline.ConsoleLog.Level; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +/** + * Log Creator for logging to a file + */ +class FileLogCreator implements LogCreator { + private final Level level; + private final Path path; + + /** + * Creates a new file Log Creator. + * + * @param commandLineArguments The command line arguments + */ + FileLogCreator(CommandLineArguments commandLineArguments) { + String outputFilepath = ""; + + if (commandLineArguments.isOutputFileSet()) { + outputFilepath = commandLineArguments.getOutputFile(); + } else if (commandLineArguments.isLogFilepathSet()) { + outputFilepath = commandLineArguments.getLogFilepath(); + } + + this.level = commandLineArguments.getLogLevel(); + this.path = Paths.get(outputFilepath); + + prepareOutputFile(path); + } + + public Log createLogger(Class clazz) { + return new FileLog(path, level); + } + + private static void prepareOutputFile(Path path) { + try { + Files.write(path, "".getBytes(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch(IOException exception) { + throw new FlywayException("Could not initialize log file at " + path + ".", exception); + } + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/Main.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/Main.java new file mode 100644 index 0000000..f11cc16 --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/Main.java @@ -0,0 +1,592 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.commandline; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.flywaydb.commandline.ConsoleLog.Level; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.*; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.flywaydb.core.api.logging.LogFactory; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.internal.info.MigrationInfoDumper; +import org.flywaydb.core.internal.jdbc.DriverDataSource; +import org.flywaydb.core.internal.license.VersionPrinter; +import org.flywaydb.core.internal.output.ErrorOutput; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.FileCopyUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.*; + +/** + * Main class and central entry point of the Flyway command-line tool. + */ +public class Main { + private static Log LOG; + + static LogCreator getLogCreator(CommandLineArguments commandLineArguments) { + // JSON output uses a different mechanism, so we do not create any loggers + if (commandLineArguments.shouldOutputJson()) { + return MultiLogCreator.empty(); + } + + List logCreators = new ArrayList<>(); + + logCreators.add(new ConsoleLogCreator(commandLineArguments)); + + if (commandLineArguments.isOutputFileSet() || commandLineArguments.isLogFilepathSet()) { + logCreators.add(new FileLogCreator(commandLineArguments)); + } + + return new MultiLogCreator(logCreators); + } + + /** + * Initializes the logging. + */ + static void initLogging(CommandLineArguments commandLineArguments) { + LogCreator logCreator = getLogCreator(commandLineArguments); + LogFactory.setFallbackLogCreator(logCreator); + LOG = LogFactory.getLog(Main.class); + } + + /** + * Main method. + * + * @param args The command-line arguments. + */ + public static void main(String[] args) { + CommandLineArguments commandLineArguments = new CommandLineArguments(args); + initLogging(commandLineArguments); + + try { + commandLineArguments.validate(LOG); + + if (commandLineArguments.shouldPrintVersionAndExit()) { + printVersion(); + System.exit(0); + } + + if (commandLineArguments.hasOperation("help") || commandLineArguments.shouldPrintUsage()) { + printUsage(); + return; + } + + Map envVars = ConfigUtils.environmentVariablesToPropertyMap(); + + Map config = new HashMap<>(); + initializeDefaults(config, commandLineArguments); + loadConfigurationFromConfigFiles(config, commandLineArguments, envVars); + config.putAll(readConfigFromInputStream(System.in)); + + if (commandLineArguments.isWorkingDirectorySet()) { + makeRelativeLocationsBasedOnWorkingDirectory(commandLineArguments, config); + } + + config.putAll(envVars); + config = overrideConfiguration(config, commandLineArguments.getConfiguration()); + + if (!commandLineArguments.shouldSuppressPrompt()) { + promptForCredentialsIfMissing(config); + } + + ConfigUtils.dumpConfiguration(config); + + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + List jarFiles = new ArrayList<>(); + jarFiles.addAll(getJdbcDriverJarFiles()); + jarFiles.addAll(getJavaMigrationJarFiles(config)); + if (!jarFiles.isEmpty()) { + classLoader = ClassUtils.addJarsOrDirectoriesToClasspath(classLoader, jarFiles); + } + filterProperties(config); + Flyway flyway = Flyway.configure(classLoader).configuration(config).load(); + + for (String operation : commandLineArguments.getOperations()) { + executeOperation(flyway, operation, commandLineArguments); + } + } catch (Exception e) { + if (commandLineArguments.shouldOutputJson()) { + ErrorOutput errorOutput = ErrorOutput.fromException(e); + printJson(commandLineArguments, errorOutput); + } else { + if (commandLineArguments.getLogLevel() == Level.DEBUG) { + LOG.error("Unexpected error", e); + } else { + LOG.error(getMessagesFromException(e)); + } + } + System.exit(1); + } + } + + private static void makeRelativeLocationsBasedOnWorkingDirectory(CommandLineArguments commandLineArguments, Map config) { + String[] locations = config.get(ConfigUtils.LOCATIONS).split(","); + for (int i = 0; i < locations.length; i++) { + if (locations[i].startsWith(Location.FILESYSTEM_PREFIX)) { + String newLocation = locations[i].substring(Location.FILESYSTEM_PREFIX.length()); + File file = new File(newLocation); + if (!file.isAbsolute()) { + file = new File(commandLineArguments.getWorkingDirectory(), newLocation); + } + locations[i] = Location.FILESYSTEM_PREFIX + file.getAbsolutePath(); + } + } + + config.put(ConfigUtils.LOCATIONS, StringUtils.arrayToCommaDelimitedString(locations)); + } + + private static Map overrideConfiguration(Map existingConfiguration, Map newConfiguration) { + Map combinedConfiguration = new HashMap<>(); + + combinedConfiguration.putAll(existingConfiguration); + combinedConfiguration.putAll(newConfiguration); + + return combinedConfiguration; + } + + + static String getMessagesFromException(Throwable e) { + String condensedMessages = ""; + String preamble = ""; + while (e != null) { + if (e instanceof FlywayException) { + condensedMessages += preamble + e.getMessage(); + } else { + condensedMessages += preamble + e.toString(); + } + preamble = "\r\nCaused by: "; + e = e.getCause(); + } + return condensedMessages; + } + + + /** + * Executes this operation on this Flyway instance. + * + * @param flyway The Flyway instance. + * @param operation The operation to execute. + */ + private static void executeOperation(Flyway flyway, String operation, CommandLineArguments commandLineArguments) { + if ("clean".equals(operation)) { + flyway.clean(); + } else if ("baseline".equals(operation)) { + flyway.baseline(); + } else if ("migrate".equals(operation)) { + flyway.migrate(); + } else if ("undo".equals(operation)) { + flyway.undo(); + } else if ("validate".equals(operation)) { + flyway.validate(); + } else if ("info".equals(operation)) { + MigrationInfoService info = flyway.info(); + MigrationInfo current = info.current(); + MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion(); + + MigrationVersion schemaVersionToOutput = currentSchemaVersion == null ? MigrationVersion.EMPTY : currentSchemaVersion; + LOG.info("Schema version: " + schemaVersionToOutput); + LOG.info(""); + LOG.info(MigrationInfoDumper.dumpToAsciiTable(info.all())); + + if (commandLineArguments.shouldOutputJson()) { + printJson(commandLineArguments, info.getInfoOutput()); + } + } else if ("repair".equals(operation)) { + flyway.repair(); + } else { + LOG.error("Invalid operation: " + operation); + printUsage(); + System.exit(1); + } + } + + private static void printJson(CommandLineArguments commandLineArguments, Object object) { + String json = convertObjectToJsonString(object); + + if (commandLineArguments.isOutputFileSet()) { + Path path = Paths.get(commandLineArguments.getOutputFile()); + byte[] bytes = json.getBytes(); + + try { + Files.write(path, bytes, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + throw new FlywayException("Could not write to output file " + commandLineArguments.getOutputFile(), e); + } + } + + System.out.println(json); + } + + private static String convertObjectToJsonString(Object object) { + Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + return gson.toJson(object); + } + + /** + * Initializes the config with the default configuration for the command-line tool. + * + * @param config The config object to initialize. + */ + private static void initializeDefaults(Map config, CommandLineArguments commandLineArguments) { + // To maintain override order, return extension value first if present + String workingDirectory = commandLineArguments.isWorkingDirectorySet() ? commandLineArguments.getWorkingDirectory() : getInstallationDir(); + + config.put(ConfigUtils.LOCATIONS, "filesystem:" + new File(workingDirectory, "sql").getAbsolutePath()); + config.put(ConfigUtils.JAR_DIRS, new File(workingDirectory, "jars").getAbsolutePath()); + } + + + /** + * Filters there properties to remove the Flyway Commandline-specific ones. + * + * @param config The properties to filter. + */ + private static void filterProperties(Map config) { + config.remove(ConfigUtils.JAR_DIRS); + config.remove(ConfigUtils.CONFIG_FILES); + config.remove(ConfigUtils.CONFIG_FILE_ENCODING); + } + + /** + * Prints the version number on the console. + */ + private static void printVersion() { + VersionPrinter.printVersionOnly(); + LOG.info(""); + + LOG.debug("Java " + System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")"); + LOG.debug(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch") + "\n"); + } + + /** + * Prints the usage instructions on the console. + */ + private static void printUsage() { + LOG.info("Usage"); + LOG.info("====="); + LOG.info(""); + LOG.info("flyway [options] command"); + LOG.info(""); + LOG.info("By default, the configuration will be read from conf/flyway.conf."); + LOG.info("Options passed from the command-line override the configuration."); + LOG.info(""); + LOG.info("Commands"); + LOG.info("--------"); + LOG.info("migrate : Migrates the database"); + LOG.info("clean : Drops all objects in the configured schemas"); + LOG.info("info : Prints the information about applied, current and pending migrations"); + LOG.info("validate : Validates the applied migrations against the ones on the classpath"); + LOG.info("undo : [" + "pro] Undoes the most recently applied versioned migration"); + LOG.info("baseline : Baselines an existing database at the baselineVersion"); + LOG.info("repair : Repairs the schema history table"); + LOG.info(""); + LOG.info("Options (Format: -key=value)"); + LOG.info("-------"); + LOG.info("driver : Fully qualified classname of the JDBC driver"); + LOG.info("url : Jdbc url to use to connect to the database"); + LOG.info("user : User to use to connect to the database"); + LOG.info("password : Password to use to connect to the database"); + LOG.info("connectRetries : Maximum number of retries when attempting to connect to the database"); + LOG.info("initSql : SQL statements to run to initialize a new database connection"); + LOG.info("schemas : Comma-separated list of the schemas managed by Flyway"); + LOG.info("table : Name of Flyway's schema history table"); + LOG.info("locations : Classpath locations to scan recursively for migrations"); + LOG.info("resolvers : Comma-separated list of custom MigrationResolvers"); + LOG.info("skipDefaultResolvers : Skips default resolvers (jdbc, sql and Spring-jdbc)"); + LOG.info("sqlMigrationPrefix : File name prefix for versioned SQL migrations"); + LOG.info("undoSqlMigrationPrefix : [" + "pro] File name prefix for undo SQL migrations"); + LOG.info("repeatableSqlMigrationPrefix : File name prefix for repeatable SQL migrations"); + LOG.info("sqlMigrationSeparator : File name separator for SQL migrations"); + LOG.info("sqlMigrationSuffixes : Comma-separated list of file name suffixes for SQL migrations"); + LOG.info("stream : [" + "pro] Stream SQL migrations when executing them"); + LOG.info("batch : [" + "pro] Batch SQL statements when executing them"); + LOG.info("mixed : Allow mixing transactional and non-transactional statements"); + LOG.info("encoding : Encoding of SQL migrations"); + LOG.info("placeholderReplacement : Whether placeholders should be replaced"); + LOG.info("placeholders : Placeholders to replace in sql migrations"); + LOG.info("placeholderPrefix : Prefix of every placeholder"); + LOG.info("placeholderSuffix : Suffix of every placeholder"); + LOG.info("installedBy : Username that will be recorded in the schema history table"); + LOG.info("target : Target version up to which Flyway should use migrations"); + LOG.info("outOfOrder : Allows migrations to be run \"out of order\""); + LOG.info("callbacks : Comma-separated list of FlywayCallback classes"); + LOG.info("skipDefaultCallbacks : Skips default callbacks (sql)"); + LOG.info("validateOnMigrate : Validate when running migrate"); + LOG.info("validateMigrationNaming : Validate file names of SQL migrations (including callbacks)"); + LOG.info("ignoreMissingMigrations : Allow missing migrations when validating"); + LOG.info("ignoreIgnoredMigrations : Allow ignored migrations when validating"); + LOG.info("ignorePendingMigrations : Allow pending migrations when validating"); + LOG.info("ignoreFutureMigrations : Allow future migrations when validating"); + LOG.info("cleanOnValidationError : Automatically clean on a validation error"); + LOG.info("cleanDisabled : Whether to disable clean"); + LOG.info("baselineVersion : Version to tag schema with when executing baseline"); + LOG.info("baselineDescription : Description to tag schema with when executing baseline"); + LOG.info("baselineOnMigrate : Baseline on migrate against uninitialized non-empty schema"); + LOG.info("configFiles : Comma-separated list of config files to use"); + LOG.info("configFileEncoding : Encoding to use when loading the config files"); + LOG.info("jarDirs : Comma-separated list of dirs for Jdbc drivers & Java migrations"); + LOG.info("createSchemas : Whether Flyway should attempt to create the schemas specified in the schemas property"); + LOG.info("dryRunOutput : [" + "pro] File where to output the SQL statements of a migration dry run"); + LOG.info("errorOverrides : [" + "pro] Rules to override specific SQL states and errors codes"); + LOG.info("oracle.sqlplus : [" + "pro] Enable Oracle SQL*Plus command support"); + LOG.info("licenseKey : [" + "pro] Your Flyway license key"); + LOG.info("color : Whether to colorize output. Values: always, never, or auto (default)"); + LOG.info("outputFile : Send output to the specified file alongside the console"); + LOG.info(""); + LOG.info("Flags"); + LOG.info("-----"); + LOG.info("-X : Print debug output"); + LOG.info("-q : Suppress all output, except for errors and warnings"); + LOG.info("-n : Suppress prompting for a user and password"); + LOG.info("-v : Print the Flyway version and exit"); + LOG.info("-? : Print this usage info and exit"); + LOG.info("-json : Print the output in JSON format"); + LOG.info("-community : Run the Flyway Community Edition (default)"); + LOG.info("-pro : Run the Flyway Pro Edition"); + LOG.info("-enterprise : Run the Flyway Enterprise Edition"); + LOG.info(""); + LOG.info("Example"); + LOG.info("-------"); + LOG.info("flyway -user=myuser -password=s3cr3t -url=jdbc:h2:mem -placeholders.abc=def migrate"); + LOG.info(""); + LOG.info("More info at https://flywaydb.org/documentation/commandline"); + } + + /** + * Gets the jar files of all the JDBC drivers contained in the drivers folder. + * + * @return The jar files. + */ + private static List getJdbcDriverJarFiles() { + File driversDir = new File(getInstallationDir(), "drivers"); + File[] files = driversDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".jar"); + } + }); + + // see javadoc of listFiles(): null if given path is not a real directory + if (files == null) { + LOG.debug("Directory for Jdbc Drivers not found: " + driversDir.getAbsolutePath()); + return Collections.emptyList(); + } + + return Arrays.asList(files); + } + + /** + * Gets all the jar files contained in the jars folder. (For Java Migrations) + * + * @param config The configured properties. + * @return The jar files. + */ + private static List getJavaMigrationJarFiles(Map config) { + String jarDirs = config.get(ConfigUtils.JAR_DIRS); + if (!StringUtils.hasLength(jarDirs)) { + return Collections.emptyList(); + } + + jarDirs = jarDirs.replace(File.pathSeparator, ","); + String[] dirs = StringUtils.tokenizeToStringArray(jarDirs, ","); + + List jarFiles = new ArrayList<>(); + for (String dirName : dirs) { + File dir = new File(dirName); + File[] files = dir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".jar"); + } + }); + + // see javadoc of listFiles(): null if given path is not a real directory + if (files == null) { + LOG.error("Directory for Java Migrations not found: " + dirName); + System.exit(1); + } + + jarFiles.addAll(Arrays.asList(files)); + } + + return jarFiles; + } + + /** + * Loads the configuration from the various possible locations. + * + * @param config The properties object to load to configuration into. + * @param commandLineArguments The command-line arguments passed in. + * @param envVars The environment variables, converted into properties. + */ + /* private -> for testing */ + static void loadConfigurationFromConfigFiles(Map config, CommandLineArguments commandLineArguments, Map envVars) { + String encoding = determineConfigurationFileEncoding(commandLineArguments, envVars); + File installationDir = new File(getInstallationDir()); + + config.putAll(ConfigUtils.loadDefaultConfigurationFiles(installationDir, encoding)); + + for (File configFile : determineConfigFilesFromArgs(commandLineArguments, envVars)) { + config.putAll(ConfigUtils.loadConfigurationFile(configFile, encoding, true)); + } + } + + /* private -> for testing */ + static Map readConfigFromInputStream(InputStream inputStream) { + Map config = new HashMap<>(); + + try { + // System.in.available() : returns an estimate of the number of bytes that can be read (or skipped over) from this input stream + // Used to check if there is any data in the stream + if (inputStream != null && inputStream.available() > 0) { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + LOG.debug("Attempting to load configuration from standard input"); + int firstCharacter = bufferedReader.read(); + + if (bufferedReader.ready() && firstCharacter != -1) { + // Prepend the first character to the rest of the string + // This is a char, represented as an int, so we cast to a char + // which is implicitly converted to an string + String configurationString = (char)firstCharacter + FileCopyUtils.copyToString(bufferedReader); + Map configurationFromStandardInput = ConfigUtils.loadConfigurationFromString(configurationString); + + if (configurationFromStandardInput.isEmpty()) { + LOG.debug("Empty configuration provided from standard input"); + } else { + LOG.info("Loaded configuration from standard input"); + config.putAll(configurationFromStandardInput); + } + } else { + LOG.debug("Could not load configuration from standard input"); + } + } + } catch (Exception e) { + LOG.debug("Could not load configuration from standard input " + e.getMessage()); + } + + return config; + } + + /** + * If no user or password has been provided, prompt for it. If you want to avoid the prompt, + * pass in an empty user or password. + * + * @param config The properties object to load to configuration into. + */ + private static void promptForCredentialsIfMissing(Map config) { + Console console = System.console(); + if (console == null) { + // We are running in an automated build. Prompting is not possible. + return; + } + + if (!config.containsKey(ConfigUtils.URL)) { + // URL is not set. We are doomed for failure anyway. + return; + } + + String url = config.get(ConfigUtils.URL); + if (!config.containsKey(ConfigUtils.USER) && needsUser(url)) { + config.put(ConfigUtils.USER, console.readLine("Database user: ")); + } + + if (!config.containsKey(ConfigUtils.PASSWORD) && needsPassword(url)) { + char[] password = console.readPassword("Database password: "); + config.put(ConfigUtils.PASSWORD, password == null ? "" : String.valueOf(password)); + } + } + + /** + * Detect whether the JDBC URL specifies a known authentication mechanism that does not need a username. + */ + private static boolean needsUser(String url) { + return DriverDataSource.detectUserRequiredByUrl(url); + } + + /** + * Detect whether the JDBC URL specifies a known authentication mechanism that does not need a password. + */ + private static boolean needsPassword(String url) { + return DriverDataSource.detectPasswordRequiredByUrl(url); + } + + /** + * Determines the files to use for loading the configuration. + * + * @param commandLineArguments The command-line arguments passed in. + * @param envVars The environment variables converted to Flyway properties. + * @return The configuration files. + */ + private static List determineConfigFilesFromArgs(CommandLineArguments commandLineArguments, Map envVars) { + List configFiles = new ArrayList<>(); + + String workingDirectory = commandLineArguments.isWorkingDirectorySet() ? commandLineArguments.getWorkingDirectory() : null; + + if (envVars.containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(envVars.get(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(new File(workingDirectory, file)); + } + return configFiles; + } + + + for (String file : commandLineArguments.getConfigFiles()) { + configFiles.add(new File(workingDirectory, file)); + } + + return configFiles; + } + + /** + * @return The installation directory of the Flyway Command-line tool. + */ + @SuppressWarnings("ConstantConditions") + private static String getInstallationDir() { + String path = ClassUtils.getLocationOnDisk(Main.class); + return new File(path) // jar file + .getParentFile() // edition dir + .getParentFile() // lib dir + .getParentFile() // installation dir + .getAbsolutePath(); + } + + /** + * Determines the encoding to use for loading the configuration. + * + * @param commandLineArguments The command-line arguments passed in. + * @param envVars The environment variables converted to Flyway properties. + * @return The encoding. (default: UTF-8) + */ + private static String determineConfigurationFileEncoding(CommandLineArguments commandLineArguments, Map envVars) { + if (envVars.containsKey(ConfigUtils.CONFIG_FILE_ENCODING)) { + return envVars.get(ConfigUtils.CONFIG_FILE_ENCODING); + } + + if (commandLineArguments.isConfigFileEncodingSet()) { + return commandLineArguments.getConfigFileEncoding(); + } + + return "UTF-8"; + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogCreator.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogCreator.java new file mode 100644 index 0000000..fa97279 --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogCreator.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.commandline; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Log creator for a MultiLog + */ +class MultiLogCreator implements LogCreator { + private final List logCreators; + + MultiLogCreator(List logCreators) { + this.logCreators = logCreators; + } + + @Override + public Log createLogger(Class clazz) { + List logs = new ArrayList<>(); + + for (LogCreator logCreator : logCreators) { + logs.add(logCreator.createLogger(clazz)); + } + + return new MultiLogger(logs); + } + + static MultiLogCreator empty() { + return new MultiLogCreator(new ArrayList<>()); + } +} \ No newline at end of file diff --git b/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogger.java a/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogger.java new file mode 100644 index 0000000..4aedaaa --- /dev/null +++ a/flyway-commandline/src/main/java/org/flywaydb/commandline/MultiLogger.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.commandline; + +import org.flywaydb.core.api.logging.Log; + +import java.util.List; + +/** + * Log implementation that forwards method calls to multiple implementations + */ +class MultiLogger implements Log { + + private final List logs; + + public MultiLogger(List logs) { + this.logs = logs; + } + + @Override + public boolean isDebugEnabled() { + for (Log log : logs) { + if (!log.isDebugEnabled()) { + return false; + } + } + + return true; + } + + @Override + public void debug(String message) { + for (Log log : logs) { + log.debug(message); + } + } + + @Override + public void info(String message) { + for (Log log : logs) { + log.info(message); + } + } + + @Override + public void warn(String message) { + for (Log log : logs) { + log.warn(message); + } + } + + @Override + public void error(String message) { + for (Log log : logs) { + log.error(message); + } + } + + @Override + public void error(String message, Exception e) { + for (Log log : logs) { + log.error(message, e); + } + } +} \ No newline at end of file diff --git b/flyway-core/pom.xml a/flyway-core/pom.xml new file mode 100644 index 0000000..8f820b3 --- /dev/null +++ a/flyway-core/pom.xml @@ -0,0 +1,355 @@ + + + 4.0.0 + + org.flywaydb + flyway-parent + 0-SNAPSHOT + + flyway-core + jar + ${project.artifactId} + + + 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 b/flyway-core/src/main/java/org/flywaydb/core/Flyway.java a/flyway-core/src/main/java/org/flywaydb/core/Flyway.java new file mode 100644 index 0000000..a27c780 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/Flyway.java @@ -0,0 +1,730 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.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.api.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.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 { + if (configuration.getCreateSchemas()) { + new DbSchemas(database, schemas, schemaHistory).create(false); + } else { + LOG.warn("The configuration option 'createSchemas' is false.\n" + + "However the schema history table still needs a schema to reside in.\n" + + "You must manually create a schema for the schema history table to reside in.\n" + + "See http://flywaydb.org/documentation/migrations#the-createschemas-option-and-the-schema-history-table)"); + } + + 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 + + + + ) { + if (configuration.getCreateSchemas()) { + new DbSchemas(database, schemas, schemaHistory).create(true); + } else { + LOG.warn("The configuration option 'createSchemas' is false.\n" + + "Even though Flyway is configured not to create any schemas, the schema history table still needs a schema to reside in.\n" + + "You must manually create a schema for the schema history table to reside in.\n" + + "See http://flywaydb.org/documentation/migrations#the-createschemas-option-and-the-schema-history-table"); + } + + 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 Pair> resourceProviderClassProviderPair = createResourceAndClassProviders(scannerRequired); + final ResourceProvider resourceProvider = resourceProviderClassProviderPair.getLeft(); + final ClassProvider classProvider = resourceProviderClassProviderPair.getRight(); + + 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 Pair> createResourceAndClassProviders(boolean scannerRequired) { + ResourceProvider resourceProvider; + ClassProvider classProvider; + if (!scannerRequired && configuration.isSkipDefaultResolvers() && configuration.isSkipDefaultCallbacks()) { + resourceProvider = NoopResourceProvider.INSTANCE; + //noinspection unchecked + classProvider = NoopClassProvider.INSTANCE; + } else { + if (configuration.getResourceProvider() != null && configuration.getJavaMigrationClassProvider() != null) { + // don't create the scanner at all in this case + resourceProvider = configuration.getResourceProvider(); + classProvider = configuration.getJavaMigrationClassProvider(); + } else { + Scanner scanner = new Scanner<>( + JavaMigration.class, + Arrays.asList(configuration.getLocations()), + configuration.getClassLoader(), + configuration.getEncoding() + + + + , resourceNameCache + , locationScannerCache + ); + // set the defaults + resourceProvider = scanner; + classProvider = scanner; + if (configuration.getResourceProvider() != null) { + resourceProvider = configuration.getResourceProvider(); + } + if (configuration.getJavaMigrationClassProvider() != null) { + classProvider = configuration.getJavaMigrationClassProvider(); + } + } + } + + return Pair.of(resourceProvider, classProvider); + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/api/ClassProvider.java a/flyway-core/src/main/java/org/flywaydb/core/api/ClassProvider.java new file mode 100644 index 0000000..e02bc11 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/api/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.api; + +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 b/flyway-core/src/main/java/org/flywaydb/core/api/ErrorCode.java a/flyway-core/src/main/java/org/flywaydb/core/api/ErrorCode.java new file mode 100644 index 0000000..035396c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/FlywayException.java a/flyway-core/src/main/java/org/flywaydb/core/api/FlywayException.java new file mode 100644 index 0000000..1d53a8b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/InfoOutputProvider.java a/flyway-core/src/main/java/org/flywaydb/core/api/InfoOutputProvider.java new file mode 100644 index 0000000..2b7016d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/Location.java a/flyway-core/src/main/java/org/flywaydb/core/api/Location.java new file mode 100644 index 0000000..d800c08 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfo.java a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfo.java new file mode 100644 index 0000000..a7895ae --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfoService.java a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationInfoService.java new file mode 100644 index 0000000..7888461 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationState.java a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationState.java new file mode 100644 index 0000000..1e27976 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationType.java a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationType.java new file mode 100644 index 0000000..02e03fa --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/MigrationVersion.java a/flyway-core/src/main/java/org/flywaydb/core/api/MigrationVersion.java new file mode 100644 index 0000000..2f08f0f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/ResourceProvider.java a/flyway-core/src/main/java/org/flywaydb/core/api/ResourceProvider.java new file mode 100644 index 0000000..9fda9bb --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/api/ResourceProvider.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.api; + +import org.flywaydb.core.internal.resource.LoadableResource; + +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 b/flyway-core/src/main/java/org/flywaydb/core/api/android/ContextHolder.java a/flyway-core/src/main/java/org/flywaydb/core/api/android/ContextHolder.java new file mode 100644 index 0000000..b10abcd --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/android/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/android/package-info.java new file mode 100644 index 0000000..694c407 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/BaseCallback.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/BaseCallback.java new file mode 100644 index 0000000..07fdf00 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Callback.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Callback.java new file mode 100644 index 0000000..eae83cf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Context.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Context.java new file mode 100644 index 0000000..0c9acb9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Error.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Error.java new file mode 100644 index 0000000..14d96c5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Event.java new file mode 100644 index 0000000..7cc501d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Statement.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Statement.java new file mode 100644 index 0000000..891b02b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/Warning.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/Warning.java new file mode 100644 index 0000000..eaec707 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/callback/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/callback/package-info.java new file mode 100644 index 0000000..b92cd6f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java new file mode 100644 index 0000000..cbca07f --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/ClassicConfiguration.java @@ -0,0 +1,1985 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.ClassProvider; +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.api.ResourceProvider; +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"; + + + + + + + + + + + + + /** + * Custom Resource provider to use when looking up resources + */ + private ResourceProvider resourceProvider = null; + + + /** + * Custom ClassProvider for looking up JavaMigration classes + */ + private ClassProvider javaMigrationClassProvider = null; + + /** + * 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; + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + *

+ * {@code true} if flyway should create the schemas. {@code false} if flyway should not. (default: {@code true}) + */ + private boolean createSchemas = true; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /** + * 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"); + + + + + } + + @Override + public ResourceProvider getResourceProvider() { + return resourceProvider; + } + + @Override + public ClassProvider getJavaMigrationClassProvider() { + return javaMigrationClassProvider; + } + + @Override + public boolean getCreateSchemas() { + return createSchemas; + } + + /** + * 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"); + + + + + } + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + * + * @param createSchemas @{code true} to attempt to create the schemas (default: {@code true}) + */ + public void setShouldCreateSchemas(boolean createSchemas) { + this.createSchemas = createSchemas; + } + + /** + * 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 + "."); + + + + + + } + + public void setResourceProvider(ResourceProvider resourceProvider) { + this.resourceProvider = resourceProvider; + } + + public void setJavaMigrationClassProvider(ClassProvider javaMigrationClassProvider) { + this.javaMigrationClassProvider = javaMigrationClassProvider; + } + + /** + * 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()); + setResourceProvider(configuration.getResourceProvider()); + setJavaMigrationClassProvider(configuration.getJavaMigrationClassProvider()); + setShouldCreateSchemas(configuration.getCreateSchemas()); + } + + /** + * 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); + } + + Boolean createSchemasProp = removeBoolean(props, ConfigUtils.CREATE_SCHEMAS); + if (createSchemasProp != null) { + setShouldCreateSchemas(createSchemasProp); + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java new file mode 100644 index 0000000..fa8a0f3 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/Configuration.java @@ -0,0 +1,543 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.flywaydb.core.api.ClassProvider; +import org.flywaydb.core.api.ResourceProvider; + +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(); + + /** + * Retrieves the custom ResourceProvider to be used to look up resources. If not set, the default strategy will be used. + * + * @return The custom ResourceProvider to be used to look up resources + * (default: null) + */ + ResourceProvider getResourceProvider(); + + /** + * Retrieves the custom ClassProvider to be used to look up {@link JavaMigration} classes. If not set, the default strategy will be used. + * + * @return The custom ClassProvider to be used to look up {@link JavaMigration} classes + * (default: null) + */ + ClassProvider getJavaMigrationClassProvider(); + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + * + * @return @{code true} to attempt to create the schemas (default: {@code true}) + */ + boolean getCreateSchemas(); +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java new file mode 100644 index 0000000..3c1758c --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/FluentConfiguration.java @@ -0,0 +1,1146 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.ClassProvider; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.api.ResourceProvider; +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.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 ResourceProvider getResourceProvider() { + return config.getResourceProvider(); + } + + @Override + public ClassProvider getJavaMigrationClassProvider() { + return config.getJavaMigrationClassProvider(); + } + + @Override + public boolean outputQueryResults() { + return config.outputQueryResults(); + } + + @Override + public boolean getCreateSchemas() { + return config.getCreateSchemas(); + } + + /** + * 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; + } + + /** + * Custom ResourceProvider to be used to look up resources. If not set, the default strategy will be used. + * + * @param resourceProvider Custom ResourceProvider to be used to look up resources + */ + public FluentConfiguration resourceProvider(ResourceProvider resourceProvider) { + config.setResourceProvider(resourceProvider); + return this; + } + + /** + * Custom ClassProvider to be used to look up {@link JavaMigration} classes. If not set, the default strategy will be used. + * + * @param javaMigrationClassProvider Custom ClassProvider to be used to look up {@link JavaMigration} classes. + */ + public FluentConfiguration javaMigrationClassProvider(ClassProvider javaMigrationClassProvider) { + config.setJavaMigrationClassProvider(javaMigrationClassProvider); + 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; + } + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + * + * @param createSchemas @{code true} to attempt to create the schemas (default: {@code true}) + */ + public FluentConfiguration createSchemas(boolean createSchemas) { + config.setShouldCreateSchemas(createSchemas); + 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 b/flyway-core/src/main/java/org/flywaydb/core/api/configuration/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/configuration/package-info.java new file mode 100644 index 0000000..e360e7d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/executor/Context.java a/flyway-core/src/main/java/org/flywaydb/core/api/executor/Context.java new file mode 100644 index 0000000..c682487 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/executor/MigrationExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/api/executor/MigrationExecutor.java new file mode 100644 index 0000000..a200c0e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/executor/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/executor/package-info.java new file mode 100644 index 0000000..d25f38f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/logging/Log.java a/flyway-core/src/main/java/org/flywaydb/core/api/logging/Log.java new file mode 100644 index 0000000..8332656 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogCreator.java a/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogCreator.java new file mode 100644 index 0000000..8e04e2a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java a/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java new file mode 100644 index 0000000..357a6f5 --- /dev/null +++ a/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 volatile 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 b/flyway-core/src/main/java/org/flywaydb/core/api/logging/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/logging/package-info.java new file mode 100644 index 0000000..1555fa8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/migration/BaseJavaMigration.java a/flyway-core/src/main/java/org/flywaydb/core/api/migration/BaseJavaMigration.java new file mode 100644 index 0000000..9eb31a1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/migration/Context.java a/flyway-core/src/main/java/org/flywaydb/core/api/migration/Context.java new file mode 100644 index 0000000..6dd9666 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/migration/JavaMigration.java a/flyway-core/src/main/java/org/flywaydb/core/api/migration/JavaMigration.java new file mode 100644 index 0000000..d2c699c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/migration/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/migration/package-info.java new file mode 100644 index 0000000..d6d590e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/package-info.java new file mode 100644 index 0000000..f7ea4c0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ChecksumMatcher.java a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ChecksumMatcher.java new file mode 100644 index 0000000..2986649 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/Context.java a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/Context.java new file mode 100644 index 0000000..62b46df --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/MigrationResolver.java a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/MigrationResolver.java new file mode 100644 index 0000000..1c6b9bf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ResolvedMigration.java a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/ResolvedMigration.java new file mode 100644 index 0000000..b5bf08f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/api/resolver/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/api/resolver/package-info.java new file mode 100644 index 0000000..13fd55a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/CallbackExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/CallbackExecutor.java new file mode 100644 index 0000000..1bf6b07 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/DefaultCallbackExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/DefaultCallbackExecutor.java new file mode 100644 index 0000000..00849fb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallback.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallback.java new file mode 100644 index 0000000..99ac6a4 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallbackExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/NoopCallbackExecutor.java new file mode 100644 index 0000000..90b2f2d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SimpleContext.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SimpleContext.java new file mode 100644 index 0000000..004e4bd --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SqlScriptCallbackFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/SqlScriptCallbackFactory.java new file mode 100644 index 0000000..f71f516 --- /dev/null +++ a/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.api.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/callback/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/callback/package-info.java new file mode 100644 index 0000000..b189d1e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.java a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.java new file mode 100644 index 0000000..0c5bc49 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/NoopClassProvider.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.clazz; + +import org.flywaydb.core.api.ClassProvider; + +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 b/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/clazz/package-info.java new file mode 100644 index 0000000..85d2497 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbBaseline.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbBaseline.java new file mode 100644 index 0000000..8993977 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbClean.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbClean.java new file mode 100644 index 0000000..4e39fb7 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbInfo.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbInfo.java new file mode 100644 index 0000000..a2afdc1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbMigrate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbMigrate.java new file mode 100644 index 0000000..3a6f9f6 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbRepair.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbRepair.java new file mode 100644 index 0000000..5ae649d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbSchemas.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbSchemas.java new file mode 100644 index 0000000..395fd5a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbValidate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/DbValidate.java new file mode 100644 index 0000000..fab43fb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/command/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/command/package-info.java new file mode 100644 index 0000000..b3f88bb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.java new file mode 100644 index 0000000..60643b4 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigUtils.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.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"; + public static final String CREATE_SCHEMAS = "flyway.createSchemas"; + + // 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; + } + if ("FLYWAY_CREATE_SCHEMAS".equals(key)) { + return CREATE_SCHEMAS; + } + + // 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.java new file mode 100644 index 0000000..284316f --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/ConfigurationValidator.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.configuration; + +import org.flywaydb.core.api.ErrorCode; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.Configuration; + +import java.util.Locale; + +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(Locale.ENGLISH).startsWith("flyway:")) { + throw new FlywayException("Invalid placeholder ('flyway:' prefix is reserved): " + key); + } + } + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/configuration/package-info.java new file mode 100644 index 0000000..19d757a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseExecutionStrategy.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseExecutionStrategy.java new file mode 100644 index 0000000..7b8ae63 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java new file mode 100644 index 0000000..bb624a3 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DatabaseFactory.java @@ -0,0 +1,395 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.ResourceProvider; +import org.flywaydb.core.internal.sqlscript.*; + +import java.sql.Connection; + +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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/DefaultExecutionStrategy.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/DefaultExecutionStrategy.java new file mode 100644 index 0000000..698e3c3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Connection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Connection.java new file mode 100644 index 0000000..db55634 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Database.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Database.java new file mode 100644 index 0000000..b2ca912 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Function.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Function.java new file mode 100644 index 0000000..1614f8b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Schema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Schema.java new file mode 100644 index 0000000..1c8d127 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/SchemaObject.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/SchemaObject.java new file mode 100644 index 0000000..c9c6134 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Table.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Table.java new file mode 100644 index 0000000..2f56fe0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Type.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/Type.java new file mode 100644 index 0000000..4fdab06 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/base/package-info.java new file mode 100644 index 0000000..de4f40e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBConnection.java new file mode 100644 index 0000000..db5479c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBDatabase.java new file mode 100644 index 0000000..fee0508 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBParser.java new file mode 100644 index 0000000..704bf4c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBRetryingStrategy.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBRetryingStrategy.java new file mode 100644 index 0000000..cb61d78 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBSchema.java new file mode 100644 index 0000000..c5d8012 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/CockroachDBTable.java new file mode 100644 index 0000000..055eb38 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/cockroachdb/package-info.java new file mode 100644 index 0000000..d41e21d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Connection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Connection.java new file mode 100644 index 0000000..6a53927 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Database.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Database.java new file mode 100644 index 0000000..f7f2697 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Function.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Function.java new file mode 100644 index 0000000..e14bc0e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Parser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Parser.java new file mode 100644 index 0000000..1abf5a7 --- /dev/null +++ a/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(keyword.getText()); + + } + } 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Schema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Schema.java new file mode 100644 index 0000000..c8ba4c8 --- /dev/null +++ a/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 + "'" + " and ROUTINEMODULENAME IS NULL"; + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Table.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Table.java new file mode 100644 index 0000000..e6e9563 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Type.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/DB2Type.java new file mode 100644 index 0000000..b1ac524 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/db2/package-info.java new file mode 100644 index 0000000..c26c8b0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyConnection.java new file mode 100644 index 0000000..f4c4a2b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyDatabase.java new file mode 100644 index 0000000..96ba081 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyParser.java new file mode 100644 index 0000000..da6d1c0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbySchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbySchema.java new file mode 100644 index 0000000..99a1a53 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/DerbyTable.java new file mode 100644 index 0000000..b639016 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/derby/package-info.java new file mode 100644 index 0000000..4b06743 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdConnection.java new file mode 100644 index 0000000..e9ceab2 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdDatabase.java new file mode 100644 index 0000000..b10c32c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdParser.java new file mode 100644 index 0000000..4dc64a2 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdSchema.java new file mode 100644 index 0000000..4292588 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/FirebirdTable.java new file mode 100644 index 0000000..d0a5d21 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/firebird/package-info.java new file mode 100644 index 0000000..97bb5b6 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Connection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Connection.java new file mode 100644 index 0000000..284a875 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Database.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Database.java new file mode 100644 index 0000000..7f9ec9f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Parser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Parser.java new file mode 100644 index 0000000..8930db1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Schema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Schema.java new file mode 100644 index 0000000..565d7e2 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Table.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/H2Table.java new file mode 100644 index 0000000..e96ecbb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/h2/package-info.java new file mode 100644 index 0000000..4683370 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBConnection.java new file mode 100644 index 0000000..1f28dd5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBDatabase.java new file mode 100644 index 0000000..8359ea0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBParser.java new file mode 100644 index 0000000..cf453bd --- /dev/null +++ a/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(keywordText); + } 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBSchema.java new file mode 100644 index 0000000..cdbc556 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/HSQLDBTable.java new file mode 100644 index 0000000..4fdd181 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/hsqldb/package-info.java new file mode 100644 index 0000000..9625093 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixConnection.java new file mode 100644 index 0000000..8999f46 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixDatabase.java new file mode 100644 index 0000000..a557408 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixParser.java new file mode 100644 index 0000000..ede119e --- /dev/null +++ a/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(previous); + } else if ("END".equals(previous)) { + context.decreaseBlockDepth(); + } + } + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixSchema.java new file mode 100644 index 0000000..2e06d7a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/InformixTable.java new file mode 100644 index 0000000..8a6c780 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/informix/package-info.java new file mode 100644 index 0000000..25ab0a0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLConnection.java new file mode 100644 index 0000000..6c26345 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java new file mode 100644 index 0000000..4828b45 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLDatabase.java @@ -0,0 +1,338 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 { + String pcx_strict_mode = jdbcTemplate.queryForString( + "select VARIABLE_VALUE from performance_schema.global_variables" + + " where variable_name = 'pxc_strict_mode'"); + if ("ENFORCING".equals(pcx_strict_mode) || "MASTER".equals(pcx_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); + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLNamedLockTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLNamedLockTemplate.java new file mode 100644 index 0000000..ccb3930 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.java new file mode 100644 index 0000000..4cc975b --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLParser.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.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 = '#'; + + public MySQLParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 8); + } + + @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.equals(tokenType) || ";".equals(token.getText())) { + return true; + } else if (TokenType.EOF.equals(tokenType)) { + return true; + } + + return super.shouldAdjustBlockDepth(context, token); + } + + 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 ("BEGIN".equals(keywordText)) { + context.increaseBlockDepth(""); + } + + if (context.getBlockDepth() > 0 && lastTokenIs(tokens, parensDepth, "END")) { + String initiator = context.getBlockInitiator(); + if (initiator.equals("") || initiator.equals(keywordText) || "AS".equals(keywordText)) { + context.decreaseBlockDepth(); + } + } + + if (";".equals(keywordText) || TokenType.DELIMITER.equals(keyword.getType()) || TokenType.EOF.equals(keyword.getType())) { + if (context.getBlockDepth() > 0 && doesDelimiterEndFunction(tokens, keyword)) { + context.decreaseBlockDepth(); + } + } + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLSchema.java new file mode 100644 index 0000000..c50408c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/MySQLTable.java new file mode 100644 index 0000000..b9e982d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/mysql/package-info.java new file mode 100644 index 0000000..a897d1d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleConnection.java new file mode 100644 index 0000000..ad75b67 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleDatabase.java new file mode 100644 index 0000000..f05b62d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java new file mode 100644 index 0000000..cd70432 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleParser.java @@ -0,0 +1,552 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.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("PLSQL_JAVA_STATEMENT"); + } else if ("}".equals(keywordText)) { + context.decreaseBlockDepth(); + } + return; + } + + int parensDepth = keyword.getParensDepth(); + + if ("BEGIN".equals(keywordText) + || (CONTROL_FLOW_KEYWORDS.contains(keywordText) && !precedingEndAttachesToThisKeyword(tokens, parensDepth, context, keywordText)) + || ("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(keywordText); + } 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; + } + } + + private boolean precedingEndAttachesToThisKeyword(List tokens, int parensDepth, ParserContext context, String keywordText) { + // Normally IF, LOOP and CASE all pair up with END IF, END LOOP, END CASE + // However, CASE ... END is valid in expressions, so in code such as + // FOR i IN 1 .. CASE WHEN foo THEN 5 ELSE 6 END + // LOOP + // ... + // END LOOP + // the first END does *not* attach to the subsequent LOOP. The same is possible with $IF ... $END constructions + return lastTokenIs(tokens, parensDepth, "END") && keywordText.equals(context.getLastClosedBlockInitiator()); + } + + @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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.java new file mode 100644 index 0000000..dfe545a --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleResults.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.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSchema.java new file mode 100644 index 0000000..a811c68 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.java new file mode 100644 index 0000000..408dc04 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleSqlScriptExecutor.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.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; +import java.util.Locale; + +@SuppressWarnings("SqlResolve") +public class OracleSqlScriptExecutor extends DefaultSqlScriptExecutor { + + + + + + + + + + + + + + + + + + + + + public OracleSqlScriptExecutor(JdbcTemplate jdbcTemplate + + + + + ) { + super(jdbcTemplate + + + + ); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/OracleTable.java new file mode 100644 index 0000000..d171523 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/oracle/package-info.java new file mode 100644 index 0000000..f48f94a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/package-info.java new file mode 100644 index 0000000..51fdfbf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLAdvisoryLockTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLAdvisoryLockTemplate.java new file mode 100644 index 0000000..d70b2ed --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.java new file mode 100644 index 0000000..88c0d12 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLConnection.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.database.postgresql; + +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; + +import java.sql.SQLException; +import java.util.concurrent.Callable; + +/** + * 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 + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.java new file mode 100644 index 0000000..a40509d --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLCopyParsedStatement.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.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java new file mode 100644 index 0000000..2e5ac91 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLDatabase.java @@ -0,0 +1,165 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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"); + + ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition("9.4", 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() + "\""; + + 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\" 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" + + (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\");"; + } + + @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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLParser.java new file mode 100644 index 0000000..b59d0f1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java new file mode 100644 index 0000000..c94b02d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLTable.java new file mode 100644 index 0000000..bd0e11e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/PostgreSQLType.java new file mode 100644 index 0000000..e0aad83 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/postgresql/package-info.java new file mode 100644 index 0000000..8820a74 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftConnection.java new file mode 100644 index 0000000..24ab27c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftDatabase.java new file mode 100644 index 0000000..d4f9e3d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftParser.java new file mode 100644 index 0000000..5a957f5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftSchema.java new file mode 100644 index 0000000..c36b728 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftTable.java new file mode 100644 index 0000000..a14705e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/RedshiftType.java new file mode 100644 index 0000000..4fffe46 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/redshift/package-info.java new file mode 100644 index 0000000..d0725aa --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAConnection.java new file mode 100644 index 0000000..d8c0b91 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANADatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANADatabase.java new file mode 100644 index 0000000..4c2cce3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANAParser.java new file mode 100644 index 0000000..a400c09 --- /dev/null +++ a/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(keyword.getText()); + } else if (doTokensMatchPattern(tokens, keyword, FUNCTION_OR_PROCEDURE_REGEX)) { + context.increaseBlockDepth("FUNCTION_OR_PROCEDURE_REGEX"); + } 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANASchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANASchema.java new file mode 100644 index 0000000..60e81b3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANATable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/SAPHANATable.java new file mode 100644 index 0000000..b8dbb70 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/saphana/package-info.java new file mode 100644 index 0000000..33f53a8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeConnection.java new file mode 100644 index 0000000..1fea4ea --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java new file mode 100644 index 0000000..242507c --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeDatabase.java @@ -0,0 +1,182 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Locale; +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(Locale.ENGLISH)); + } 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.java new file mode 100644 index 0000000..5194d90 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeParser.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.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()); + } + + @Override + protected boolean isSingleLineComment(String peek, ParserContext context, int col) { + return peek.startsWith("--") || peek.startsWith("//"); + } + +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeSchema.java new file mode 100644 index 0000000..05530c4 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/SnowflakeTable.java new file mode 100644 index 0000000..c5857b7 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/snowflake/package-info.java new file mode 100644 index 0000000..7737c73 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteConnection.java new file mode 100644 index 0000000..fe16867 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteDatabase.java new file mode 100644 index 0000000..b10cc44 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteParser.java new file mode 100644 index 0000000..c836234 --- /dev/null +++ a/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(lastKeyword); + } else if ("END".equals(lastKeyword)) { + context.decreaseBlockDepth(); + } + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteSchema.java new file mode 100644 index 0000000..11ad03b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/SQLiteTable.java new file mode 100644 index 0000000..49edf74 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlite/package-info.java new file mode 100644 index 0000000..f97a76f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerApplicationLockTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerApplicationLockTemplate.java new file mode 100644 index 0000000..fe9c9d9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerConnection.java new file mode 100644 index 0000000..01d2bd9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerDatabase.java new file mode 100644 index 0000000..d5af497 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerEngineEdition.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerEngineEdition.java new file mode 100644 index 0000000..45d55b0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerParser.java new file mode 100644 index 0000000..77bd9df --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerSchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerSchema.java new file mode 100644 index 0000000..5e34863 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/SQLServerTable.java new file mode 100644 index 0000000..20c1878 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sqlserver/package-info.java new file mode 100644 index 0000000..53de07f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEConnection.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEConnection.java new file mode 100644 index 0000000..71968b2 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEDatabase.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEDatabase.java new file mode 100644 index 0000000..ae375c4 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASEParser.java new file mode 100644 index 0000000..3cf0a1c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASESchema.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASESchema.java new file mode 100644 index 0000000..90e0e71 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASETable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/SybaseASETable.java new file mode 100644 index 0000000..1751818 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/database/sybasease/package-info.java new file mode 100644 index 0000000..de85064 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywayDbUpgradeRequiredException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywayDbUpgradeRequiredException.java new file mode 100644 index 0000000..4454909 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywaySqlException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/FlywaySqlException.java new file mode 100644 index 0000000..315a0c8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/exception/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/exception/package-info.java new file mode 100644 index 0000000..87e2737 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/AppliedMigrationAttributes.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/AppliedMigrationAttributes.java new file mode 100644 index 0000000..841cda3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoContext.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoContext.java new file mode 100644 index 0000000..6bf39b8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoDumper.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoDumper.java new file mode 100644 index 0000000..e462196 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoImpl.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoImpl.java new file mode 100644 index 0000000..63fdc99 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoServiceImpl.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/MigrationInfoServiceImpl.java new file mode 100644 index 0000000..130e7ee --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/info/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/info/package-info.java new file mode 100644 index 0000000..a152bf8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/CockroachRetryingTransactionalExecutionTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/CockroachRetryingTransactionalExecutionTemplate.java new file mode 100644 index 0000000..bca90e0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DatabaseType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DatabaseType.java new file mode 100644 index 0000000..31122e3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DriverDataSource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/DriverDataSource.java new file mode 100644 index 0000000..763a5c9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ErrorImpl.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ErrorImpl.java new file mode 100644 index 0000000..581a44a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplate.java new file mode 100644 index 0000000..d2ce131 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplateFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/ExecutionTemplateFactory.java new file mode 100644 index 0000000..aaadb33 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcConnectionFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcConnectionFactory.java new file mode 100644 index 0000000..ddc8753 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcTemplate.java new file mode 100644 index 0000000..e1e4700 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/JdbcUtils.java new file mode 100644 index 0000000..4ca3579 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/PlainExecutionTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/PlainExecutionTemplate.java new file mode 100644 index 0000000..d4e7236 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Result.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Result.java new file mode 100644 index 0000000..8edccf9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Results.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/Results.java new file mode 100644 index 0000000..8be3db3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/RowMapper.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/RowMapper.java new file mode 100644 index 0000000..014ef2e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TableLockingExecutionTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TableLockingExecutionTemplate.java new file mode 100644 index 0000000..826ce98 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TransactionalExecutionTemplate.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/TransactionalExecutionTemplate.java new file mode 100644 index 0000000..ff62f10 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/WarningImpl.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/WarningImpl.java new file mode 100644 index 0000000..9fc9393 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/jdbc/package-info.java new file mode 100644 index 0000000..05426f8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/license/Edition.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/Edition.java new file mode 100644 index 0000000..f7b7d36 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEditionUpgradeRequiredException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEditionUpgradeRequiredException.java new file mode 100644 index 0000000..206a6d9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEnterpriseUpgradeRequiredException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEnterpriseUpgradeRequiredException.java new file mode 100644 index 0000000..5d776de --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayEnterpriseUpgradeRequiredException.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 Enterprise Edition feature not supported by + * Flyway Community Edition or Flyway Pro Edition. + */ +public class FlywayEnterpriseUpgradeRequiredException extends FlywayException { + public FlywayEnterpriseUpgradeRequiredException(String feature) { + super(Edition.ENTERPRISE + " upgrade required: " + feature + + " is not supported by " + Edition.COMMUNITY + " or " + Edition.PRO + "."); + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayProUpgradeRequiredException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/FlywayProUpgradeRequiredException.java new file mode 100644 index 0000000..f08aed3 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/license/VersionPrinter.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/VersionPrinter.java new file mode 100644 index 0000000..59b3db1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/license/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/license/package-info.java new file mode 100644 index 0000000..a86c88d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/LogCreatorFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/LogCreatorFactory.java new file mode 100644 index 0000000..7ec5959 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLog.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLog.java new file mode 100644 index 0000000..78c3524 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLogCreator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/android/AndroidLogCreator.java new file mode 100644 index 0000000..c5165d1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLog.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLog.java new file mode 100644 index 0000000..eecbdc1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLogCreator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/ApacheCommonsLogCreator.java new file mode 100644 index 0000000..ac8eb40 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/apachecommons/package-info.java new file mode 100644 index 0000000..d81774f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLog.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLog.java new file mode 100644 index 0000000..2763f58 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLogCreator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/JavaUtilLogCreator.java new file mode 100644 index 0000000..15fbfa5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/javautil/package-info.java new file mode 100644 index 0000000..dd33d52 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/package-info.java new file mode 100644 index 0000000..6b5065c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLog.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLog.java new file mode 100644 index 0000000..31a0188 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLogCreator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/Slf4jLogCreator.java new file mode 100644 index 0000000..6c11517 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/logging/slf4j/package-info.java new file mode 100644 index 0000000..d77b69e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/output/ErrorOutput.java a/flyway-core/src/main/java/org/flywaydb/core/internal/output/ErrorOutput.java new file mode 100644 index 0000000..7bb81a1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutput.java a/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutput.java new file mode 100644 index 0000000..0ecadbc --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java new file mode 100644 index 0000000..fb51a6b --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/output/InfoOutputFactory.java @@ -0,0 +1,152 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sql.Connection; +import java.sql.SQLException; +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 { + Connection connection = configuration.getDataSource().getConnection(); + String catalog = connection.getCatalog(); + connection.close(); + return catalog; + } 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) { + if (strings.length == 1) { + return strings[0]; + } + + StringBuilder output = new StringBuilder(); + + for(String s : strings) { + output.append(s).append(joiner); + } + + return output.toString(); + } + + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/output/MigrationOutput.java a/flyway-core/src/main/java/org/flywaydb/core/internal/output/MigrationOutput.java new file mode 100644 index 0000000..8eb9199 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/output/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/output/package-info.java new file mode 100644 index 0000000..3a742b5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/package-info.java new file mode 100644 index 0000000..b321f05 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java new file mode 100644 index 0000000..e41f7c1 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Parser.java @@ -0,0 +1,744 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + } + + protected boolean supportsPeekingMultipleLines() { + return true; + } + + /** + * 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 UnboundedReadAheadReader( + new BufferedReader( + resource.read(), 4096)))))), + supportsPeekingMultipleLines()); + + 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 (isLetter(c, context)) { + 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 isLetter(char c, ParserContext context) { + return (c == '_' || context.isLetter(c)); + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java new file mode 100644 index 0000000..d2e48c6 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParserContext.java @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; +import java.util.Stack; + +public class ParserContext { + private int parensDepth = 0; + private Stack blockInitiators = new Stack<>(); + private String lastClosedBlockInitiator = null; + 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; + } + + // When a block is closed, retain the token that opened it, so that we can determine what the + // context of a given END is. + public String getLastClosedBlockInitiator() { + return lastClosedBlockInitiator; + } + + public void increaseBlockDepth(String blockInitiator) { + blockInitiators.push(blockInitiator); + blockDepth++; + } + + public void decreaseBlockDepth() { + if (blockDepth == 0) { + throw new FlywayException("Flyway parsing bug: unable to decrease block depth below 0"); + } + blockDepth--; + lastClosedBlockInitiator = blockInitiators.pop(); + } + + public int getBlockDepth() { + return blockDepth; + } + + public String getBlockInitiator() { + return blockInitiators.size() > 0 ? blockInitiators.peek() : ""; + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParsingContext.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/ParsingContext.java new file mode 100644 index 0000000..638f9f6 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java new file mode 100644 index 0000000..df5009f --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PeekingReader.java @@ -0,0 +1,511 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + private boolean supportsPeekingMultipleLines = true; + + PeekingReader(Reader in, boolean supportsPeekingMultipleLines) { + super(in); + this.supportsPeekingMultipleLines = supportsPeekingMultipleLines; + } + + @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 (!supportsPeekingMultipleLines && 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 { + return peek(numChars, false); + } + + /** + * Peek ahead in the stream to look at this number of characters ahead in the reader. + * + * @param numChars The number of characters. + * @param peekMultipleLines Whether the peek should go across lines or not + * @return The characters. + */ + public String peek(int numChars, boolean peekMultipleLines) 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(); + int prevR = -1; + for (int i = 0; i < numChars; i++) { + int r = peekBuffer[peekBufferOffset + i]; + if (r == -1) { + break; + } else if (peekBufferOffset + i > peekMax) { + break; + } else if (!peekMultipleLines && prevR == '\n') { + break; + } + result.append((char) r); + + prevR = 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++, true); + String lastc = c; + while (c.trim().isEmpty()) { + c = peek(i++, true); + if (c.equals(lastc)) { + // if the peek is the same as the last peek, then dont loop forever, even if its still empty. + break; + } + lastc = c; + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.java new file mode 100644 index 0000000..6790479 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PlaceholderReplacingReader.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.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; + + 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; + } + + 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 we have a previous read, then consume it + if (buffer.length() > 0) { + char c = buffer.charAt(0); + buffer.deleteCharAt(0); + return c; + } + + // else read ahead by the prefix length + 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 the buffer does not contain the prefix + if (!endsWith(buffer, prefix)) { + // if it contain data, return the first character of it + if (buffer.length() > 0) { + char c = buffer.charAt(0); + buffer.deleteCharAt(0); + return c; + } + // else return -1 + return -1; + } + // if the buffer contained the prefix, wipe the buffer + buffer.delete(0, buffer.length()); + + // begin reading ahead until we get to the suffix + StringBuilder placeholderBuilder = new StringBuilder(); + do { + int r1 = super.read(); + if (r1 == -1) { + break; + } else { + placeholderBuilder.append((char) r1); + } + } while (!endsWith(placeholderBuilder, suffix)); + + // delete the suffix from the builder + for (int i = 0; i < suffix.length(); i++) { + placeholderBuilder.deleteCharAt(placeholderBuilder.length() - 1); + } + + // look up the placeholder string + 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!"); + } + + // set the current placeholder replacement + 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); + } + + @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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTracker.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTracker.java new file mode 100644 index 0000000..5873307 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTrackingReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/PositionTrackingReader.java new file mode 100644 index 0000000..66fef67 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Recorder.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Recorder.java new file mode 100644 index 0000000..23e4a69 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/RecordingReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/RecordingReader.java new file mode 100644 index 0000000..4b91b0f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Statement.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Statement.java new file mode 100644 index 0000000..c174f3e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/StatementType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/StatementType.java new file mode 100644 index 0000000..68f92e0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Token.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/Token.java new file mode 100644 index 0000000..67967b0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/TokenType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/TokenType.java new file mode 100644 index 0000000..c6df062 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/UnboundedReadAheadReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/UnboundedReadAheadReader.java new file mode 100644 index 0000000..2d387de --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/UnboundedReadAheadReader.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 java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +public class UnboundedReadAheadReader extends FilterReader { + + protected ArrayList buffers = new ArrayList<>(); + + private int readIndex = 0; + private int markIndex = -1; + private int currentBuffersSize = 0; + + private static final int bufferSize = 512; + + protected UnboundedReadAheadReader(Reader in) { + super(in); + } + + @Override + public void mark(int readAheadLimit) throws IOException { + markIndex = readIndex; + } + + @Override + public void reset() throws IOException { + readIndex = markIndex; + freeBuffers(); + markIndex = -1; + } + + @Override + public int read() throws IOException { + if (readIndex < currentBuffersSize) { + return getValue(readIndex++); + } + + int read = in.read(); + if (markIndex != -1) { + setValue(read); + readIndex++; + currentBuffersSize++; + } + return read; + } + + private int getValue(int index) { + int buffersIndex = index / bufferSize; + int buffersOffset = index - (buffersIndex * bufferSize); + return buffers.get(buffersIndex)[buffersOffset]; + } + + private void setValue(int value) { + int buffersIndex = readIndex / bufferSize; + int buffersOffset = readIndex - (buffersIndex * bufferSize); + + if (buffersOffset == 0) { + buffers.add(new int[bufferSize]); + } + + buffers.get(buffersIndex)[buffersOffset] = value; + } + + private void freeBuffers() { + int buffersToRemove = (markIndex / bufferSize) - 1; + + for (int i = 0; i < buffersToRemove; i++) { + buffers.remove(0); + readIndex -= bufferSize; + currentBuffersSize -= bufferSize; + } + } +} \ No newline at end of file diff --git b/flyway-core/src/main/java/org/flywaydb/core/internal/parser/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/parser/package-info.java new file mode 100644 index 0000000..a54f9f6 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java new file mode 100644 index 0000000..fb8441d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/CompositeMigrationResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/CompositeMigrationResolver.java new file mode 100644 index 0000000..a6d869a --- /dev/null +++ a/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.api.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.api.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/MigrationInfoHelper.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/MigrationInfoHelper.java new file mode 100644 index 0000000..cd65573 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationComparator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationComparator.java new file mode 100644 index 0000000..c851819 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationImpl.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ResolvedMigrationImpl.java new file mode 100644 index 0000000..ba57ed1 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/FixedJavaMigrationResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/FixedJavaMigrationResolver.java new file mode 100644 index 0000000..0c804b5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/JavaMigrationExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/JavaMigrationExecutor.java new file mode 100644 index 0000000..48220f4 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ResolvedJavaMigration.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ResolvedJavaMigration.java new file mode 100644 index 0000000..662a8ab --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ScanningJavaMigrationResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/ScanningJavaMigrationResolver.java new file mode 100644 index 0000000..8cf8e31 --- /dev/null +++ a/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.api.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/java/package-info.java new file mode 100644 index 0000000..b16d420 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/package-info.java new file mode 100644 index 0000000..ad9440f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/DefaultSqlMigrationExecutorFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/DefaultSqlMigrationExecutorFactory.java new file mode 100644 index 0000000..0ad3e6a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutor.java new file mode 100644 index 0000000..5f06b85 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutorFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationExecutorFactory.java new file mode 100644 index 0000000..06f4e89 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/SqlMigrationResolver.java new file mode 100644 index 0000000..3ae95e0 --- /dev/null +++ a/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.api.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/sql/package-info.java new file mode 100644 index 0000000..34d5edd --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/LoadableResource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/LoadableResource.java new file mode 100644 index 0000000..1e3e964 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.java new file mode 100644 index 0000000..d1d5452 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/NoopResourceProvider.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.resource; + +import org.flywaydb.core.api.ResourceProvider; + +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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/Resource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/Resource.java new file mode 100644 index 0000000..be46ccb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceName.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceName.java new file mode 100644 index 0000000..f8fa705 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java new file mode 100644 index 0000000..9e285ef --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameParser.java @@ -0,0 +1,148 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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<>(); + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.java new file mode 100644 index 0000000..3c08105 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceNameValidator.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.resource; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.ResourceProvider; +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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceType.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/ResourceType.java new file mode 100644 index 0000000..8ceb06a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/StringResource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/StringResource.java new file mode 100644 index 0000000..f1bcf88 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/AndroidResource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/AndroidResource.java new file mode 100644 index 0000000..b4f4c38 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/android/package-info.java new file mode 100644 index 0000000..51d08ef --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/ClassPathResource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/ClassPathResource.java new file mode 100644 index 0000000..41cfcb8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/classpath/package-info.java new file mode 100644 index 0000000..d83416d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/FileSystemResource.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/FileSystemResource.java new file mode 100644 index 0000000..ff9e8a7 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/filesystem/package-info.java new file mode 100644 index 0000000..8c9fe25 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/resource/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/resource/package-info.java new file mode 100644 index 0000000..2a25cbc --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/LocationScannerCache.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/LocationScannerCache.java new file mode 100644 index 0000000..8b1950c --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/ResourceNameCache.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/ResourceNameCache.java new file mode 100644 index 0000000..a4c99c0 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java new file mode 100644 index 0000000..b6e8b2d --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/Scanner.java @@ -0,0 +1,141 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.api.ClassProvider; +import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.internal.scanner.android.AndroidScanner; +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.nio.file.Paths; +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<>(); + + // Lookup maps to speed up getResource + private final HashMap relativeResourceMap = new HashMap<>(); + private HashMap absoluteResourceMap = null; + + /* + * 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()); + } + } + + for (LoadableResource resource : resources) { + relativeResourceMap.put(resource.getRelativePath().toLowerCase(), resource); + } + } + + @Override + public LoadableResource getResource(String name) { + LoadableResource loadedResource = relativeResourceMap.get(name.toLowerCase()); + + if (loadedResource != null) { + return loadedResource; + } + + // Only build the HashMap and resolve the absolute paths if an + // absolute path is requested as this is really slow + // Should only ever be required for sqlplus @ + if (Paths.get(name).isAbsolute()) { + if (absoluteResourceMap == null) { + absoluteResourceMap = new HashMap<>(); + for (LoadableResource resource : resources) { + absoluteResourceMap.put(resource.getAbsolutePathOnDisk().toLowerCase(), resource); + } + } + + loadedResource = absoluteResourceMap.get(name.toLowerCase()); + + if (loadedResource != null) { + return loadedResource; + } + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/AndroidScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/AndroidScanner.java new file mode 100644 index 0000000..3a7befe --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/android/package-info.java new file mode 100644 index 0000000..a617bdb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathLocationScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathLocationScanner.java new file mode 100644 index 0000000..8ac71d8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ClassPathScanner.java new file mode 100644 index 0000000..76b74db --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/DefaultUrlResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/DefaultUrlResolver.java new file mode 100644 index 0000000..2602432 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/FileSystemClassPathLocationScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/FileSystemClassPathLocationScanner.java new file mode 100644 index 0000000..0739f3a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java new file mode 100644 index 0000000..8179ae9 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarFileClassPathLocationScanner.java @@ -0,0 +1,130 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.*; +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(Locale.ENGLISH).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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/JarUtils.java new file mode 100644 index 0000000..9cf1cdb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/OsgiClassPathLocationScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/OsgiClassPathLocationScanner.java new file mode 100644 index 0000000..758a5bf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ResourceAndClassScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/ResourceAndClassScanner.java new file mode 100644 index 0000000..6311536 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/UrlResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/UrlResolver.java new file mode 100644 index 0000000..2471074 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv2UrlResolver.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv2UrlResolver.java new file mode 100644 index 0000000..49fce0e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv3ClassPathLocationScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/JBossVFSv3ClassPathLocationScanner.java new file mode 100644 index 0000000..c6c6fbf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/jboss/package-info.java new file mode 100644 index 0000000..af27717 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/classpath/package-info.java new file mode 100644 index 0000000..dbb3124 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.java new file mode 100644 index 0000000..3c63a45 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/filesystem/FileSystemScanner.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.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 org.flywaydb.core.internal.sqlscript.SqlScriptMetadata; + +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 defaultEncoding; + + + + + + /** + * Creates a new filesystem scanner. + * + * @param encoding The encoding to use. + + + + */ + public FileSystemScanner(Charset encoding + + + + ) { + this.defaultEncoding = 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)) { + Charset encoding = defaultEncoding; + String encodingBlurb = ""; + if (new File(resourceName + ".conf").exists()) { + LoadableResource metadataResource = new FileSystemResource(location, resourceName + ".conf", defaultEncoding + + + + ); + SqlScriptMetadata metadata = SqlScriptMetadata.fromResource(metadataResource); + if (metadata.encoding() != null) { + encoding = Charset.forName(metadata.encoding()); + encodingBlurb = " (with overriding encoding " + encoding + ")"; + } + } + resources.add(new FileSystemResource(location, resourceName, encoding + + + + )); + + + LOG.debug("Found filesystem resource: " + resourceName + encodingBlurb); + } + } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/scanner/package-info.java new file mode 100644 index 0000000..84fe547 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/AppliedMigration.java a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/AppliedMigration.java new file mode 100644 index 0000000..b67caad --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/JdbcTableSchemaHistory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/JdbcTableSchemaHistory.java new file mode 100644 index 0000000..520a5a5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistory.java new file mode 100644 index 0000000..4668a25 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistoryFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/SchemaHistoryFactory.java new file mode 100644 index 0000000..343b83e --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/schemahistory/package-info.java new file mode 100644 index 0000000..2a1638d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java new file mode 100644 index 0000000..97085a3 --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/DefaultSqlScriptExecutor.java @@ -0,0 +1,297 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/Delimiter.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/Delimiter.java new file mode 100644 index 0000000..3d242b8 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/FlywaySqlScriptException.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/FlywaySqlScriptException.java new file mode 100644 index 0000000..ad2fdbc --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParsedSqlStatement.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParsedSqlStatement.java new file mode 100644 index 0000000..48f6a7a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParserSqlScript.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/ParserSqlScript.java new file mode 100644 index 0000000..a84f316 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScript.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScript.java new file mode 100644 index 0000000..1d43750 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutor.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutor.java new file mode 100644 index 0000000..421cb15 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutorFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptExecutorFactory.java new file mode 100644 index 0000000..54b4c61 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptFactory.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptFactory.java new file mode 100644 index 0000000..d679cd7 --- /dev/null +++ a/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.api.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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.java new file mode 100644 index 0000000..29e314a --- /dev/null +++ a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlScriptMetadata.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.sqlscript; + +import org.flywaydb.core.api.ResourceProvider; +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 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 static final String ENCODING = "encoding"; + + private final Boolean executeInTransaction; + private final String encoding; + + + private SqlScriptMetadata(Map metadata) { + // Make copy to prevent removing elements from the original + metadata = new HashMap<>(metadata); + this.executeInTransaction = removeBoolean(metadata, EXECUTE_IN_TRANSACTION); + this.encoding = metadata.remove(ENCODING); + + ConfigUtils.checkConfigurationForUnrecognisedProperties(metadata, null); + } + + public Boolean executeInTransaction() { + return executeInTransaction; + } + + public String encoding() { return encoding; } + + 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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatement.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatement.java new file mode 100644 index 0000000..9702f42 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatementIterator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/SqlStatementIterator.java new file mode 100644 index 0000000..b90614a --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/sqlscript/package-info.java new file mode 100644 index 0000000..9675b7f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AbbreviationUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/AbbreviationUtils.java new file mode 100644 index 0000000..caaaea2 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/AsciiTable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/AsciiTable.java new file mode 100644 index 0000000..9fb4976 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomFilter.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomFilter.java new file mode 100644 index 0000000..3060cce --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomStrippingReader.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/BomStrippingReader.java new file mode 100644 index 0000000..ac92293 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ClassUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/ClassUtils.java new file mode 100644 index 0000000..890cdfd --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/CloseableIterator.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/CloseableIterator.java new file mode 100644 index 0000000..85b6ed7 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/DateUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/DateUtils.java new file mode 100644 index 0000000..907691b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/ExceptionUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/ExceptionUtils.java new file mode 100644 index 0000000..154b634 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FeatureDetector.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/FeatureDetector.java new file mode 100644 index 0000000..33bbc6d --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/FileCopyUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/FileCopyUtils.java new file mode 100644 index 0000000..fd3caa9 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/IOUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/IOUtils.java new file mode 100644 index 0000000..ded66eb --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Locations.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/Locations.java new file mode 100644 index 0000000..f343fe5 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/Pair.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/Pair.java new file mode 100644 index 0000000..01a40dd --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/SqlCallable.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/SqlCallable.java new file mode 100644 index 0000000..830ae7b --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StopWatch.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/StopWatch.java new file mode 100644 index 0000000..ce0dddf --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/StringUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/StringUtils.java new file mode 100644 index 0000000..d9e4d55 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/TimeFormat.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/TimeFormat.java new file mode 100644 index 0000000..f287584 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/UrlUtils.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/UrlUtils.java new file mode 100644 index 0000000..6164e93 --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/internal/util/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/internal/util/package-info.java new file mode 100644 index 0000000..bc4ea6f --- /dev/null +++ a/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 b/flyway-core/src/main/java/org/flywaydb/core/package-info.java a/flyway-core/src/main/java/org/flywaydb/core/package-info.java new file mode 100644 index 0000000..e54ea48 --- /dev/null +++ a/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 b/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt a/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt new file mode 100644 index 0000000..1785151 --- /dev/null +++ a/flyway-core/src/main/resources/org/flywaydb/core/internal/version.txt @@ -0,0 +1 @@ +${pom.version} \ No newline at end of file diff --git b/flyway-gradle-plugin/pom.xml a/flyway-gradle-plugin/pom.xml new file mode 100644 index 0000000..088fd6c --- /dev/null +++ a/flyway-gradle-plugin/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + org.flywaydb + flyway-parent + 0-SNAPSHOT + + flyway-gradle-plugin + jar + ${project.artifactId} + + + ${project.groupId} + flyway-core + ${project.version} + + + org.gradle + gradle-core + ${version.gradle} + provided + + + org.gradle + gradle-plugins + ${version.gradle} + provided + + + org.codehaus.groovy + groovy-all + 2.3.9 + provided + + + javax.inject + javax.inject + 1 + provided + + + + + + + + + + + + + + + + + + + + maven-resources-plugin + + + copy-license + + copy-resources + + generate-resources + + + + .. + + LICENSE.txt + README.txt + + + + ${project.build.outputDirectory}/META-INF + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayExtension.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayExtension.java new file mode 100644 index 0000000..a7efa12 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayExtension.java @@ -0,0 +1,482 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.gradle; + +import java.util.Map; + +/** + * The flyway's configuration properties. + *

More info: https://flywaydb.org/documentation/gradle

+ */ +public class FlywayExtension { + /** + * The fully qualified classname of the jdbc driver to use to connect to the database + */ + public String driver; + + /** + * The jdbc url to use to connect to the database + */ + public String url; + + /** + * The user to use to connect to the database + */ + public String user; + + /** + * The password to use to connect to the database + */ + public String 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. + * (default: 0) + *

Also configurable with Gradle or System Property: ${flyway.connectRetries}

+ */ + public int connectRetries; + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * (default: {@code null}) + *

Also configurable with Gradle or System Property: ${flyway.initSql}

+ */ + public String initSql; + + /** + *

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.

+ *

Also configurable with Gradle or System Property: ${flyway.table}

+ */ + public String table; + + /** + *

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.

+ *

Also configurable with Gradle or System Property: ${flyway.tablespace}

+ */ + public String tablespace; + + /** + * 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).
  • + *
+ *

Also configurable with Maven or System Property: ${flyway.defaultSchema}

+ */ + public String defaultSchema; + + /** + * 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.
  • + *
+ *

Also configurable with Maven or System Property: ${flyway.schemas} (comma-separated list)

+ */ + public String[] schemas; + + /** + * The version to tag an existing schema with when executing baseline. (default: 1) + */ + public String baselineVersion; + + /** + * The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + public String baselineDescription; + + /** + * 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.

+ * (default: filesystem:src/main/resources/db/migration) + */ + public String[] locations; + + /** + * The fully qualified class names of the custom MigrationResolvers to be used in addition (default) + * or as a replacement (using skipDefaultResolvers) to the built-in ones for resolving Migrations to + * apply. + *

(default: none)

+ */ + public String[] resolvers; + + /** + * If set to true, default built-in resolvers will be skipped, only custom migration resolvers will be used. + *

(default: false)

+ */ + public Boolean skipDefaultResolvers; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.sqlMigrationPrefix}

+ */ + public String sqlMigrationPrefix; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.undoSqlMigrationPrefix}

+ */ + public String undoSqlMigrationPrefix; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.repeatableSqlMigrationPrefix}

+ */ + public String repeatableSqlMigrationPrefix; + + /** + * 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

+ */ + public 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.

+ *

Also configurable with Gradle or System Property: ${flyway.sqlMigrationSuffixes}

+ */ + public String[] sqlMigrationSuffixes; + + /** + * The encoding of Sql migrations + */ + public String encoding; + + /** + * Placeholders to replace in Sql migrations + */ + public Map placeholders; + + /** + * Whether placeholders should be replaced. + */ + public Boolean placeholderReplacement; + + /** + * The prefix of every placeholder + */ + public String placeholderPrefix; + + /** + * The suffix of every placeholder + */ + public String placeholderSuffix; + + /** + * 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 String target; + + /** + * An array of fully qualified FlywayCallback class implementations + */ + public String[] callbacks; + + /** + * If set to true, default built-in callbacks will be skipped, only custom migration callbacks will be used. + *

(default: false)

+ */ + public Boolean skipDefaultCallbacks; + + /** + * Allows migrations to be run "out of order" + */ + public Boolean outOfOrder; + + /** + * Whether Flyway should output a table with the results of queries when executing migrations (default: true). + *

Flyway Pro and Flyway Enterprise only

+ *

Also configurable with Gradle or System Property: ${flyway.outputQueryResults}

+ */ + public Boolean outputQueryResults; + + /** + * Whether to automatically call validate or not when running migrate. (default: true) + */ + public Boolean validateOnMigrate; + + /** + * 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 !


+ *

Also configurable with Gradle or System Property: ${flyway.cleanOnValidationError}

+ */ + public Boolean cleanOnValidationError; + + /** + * 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.(default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreMissingMigrations}

+ */ + public 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.(default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreIgnoredMigrations}

+ */ + public Boolean 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. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignorePendingMigrations}

+ */ + public 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}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreFutureMigrations}

+ */ + public Boolean 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. + *{@code false} to continue normally, {@code true} to fail fast with an exception. (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.validateMigrationNaming}

+ */ + public Boolean validateMigrationNaming; + + /** + * Whether to disable clean. (default: {@code false}) + *

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

+ */ + public Boolean cleanDisabled; + + /** + *

+ * 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! + *

+ *

{@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false})

+ */ + public Boolean baselineOnMigrate; + + /** + * 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.

+ *

{@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false})

+ */ + public 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})

+ */ + public 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}). + */ + public String installedBy; + + /** + * Gradle configurations that will be added to the classpath for running Flyway tasks. + * (default: compile, runtime, testCompile, testRuntime) + *

Also configurable with Gradle or System Property: ${flyway.configurations}

+ */ + public String[] configurations; + + /** + * 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}

+ *

Also configurable with Gradle or System Property: ${flyway.errorOverrides}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String[] errorOverrides; + + /** + * The file where to output the SQL statements of a migration dry run. If the file specified is in a non-existent + * directory, Flyway will create all directories and parent directories as needed. + *

{@code null} to execute the SQL statements directly against the database. (default: {@code null})

+ *

Also configurable with Gradle or System Property: ${flyway.dryRunOutput}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String dryRunOutput; + + /** + * 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. + * (default: {@code false} + *

Also configurable with Gradle or System Property: ${flyway.stream}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean stream; + + /** + * 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. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.batch}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean batch; + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.oracle.sqlplus}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean oracleSqlplus; + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.oracle.sqlplusWarn}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean oracleSqlplusWarn; + + /** + * 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. + *

Also configurable with Gradle or System Property: ${flyway.licenseKey}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String licenseKey; + + /** + * The encoding of the external config files specified with the {@code flyway.configFiles} property. (default: UTF-8). + *

Also configurable with Gradle or System Property: ${flyway.configFileEncoding}

+ */ + public String configFileEncoding; + + /** + * Config files from which to load the Flyway configuration. The names of the individual properties match the ones you would + * use as Gradle or System properties. The encoding of the files is defined by the + * flyway.configFileEncoding property, which is UTF-8 by default. Relative paths are relative to the project root. + *

Also configurable with Gradle or System Property: ${flyway.configFiles}

+ */ + public String[] configFiles; + + /** + * The working directory to consider when dealing with relative paths for both config files and locations. + * (default: basedir, the directory where the POM resides) + *

+ *

Also configurable with Gradle or System Property: ${flyway.workingDirectory}

+ */ + public String workingDirectory; + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + * + *

Also configurable with Gradle or System Property: ${flyway.createSchemas}

+ */ + public Boolean createSchemas; +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayPlugin.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayPlugin.java new file mode 100644 index 0000000..43069b9 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/FlywayPlugin.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.gradle; + +import org.flywaydb.gradle.task.FlywayBaselineTask; +import org.flywaydb.gradle.task.FlywayCleanTask; +import org.flywaydb.gradle.task.FlywayInfoTask; +import org.flywaydb.gradle.task.FlywayMigrateTask; +import org.flywaydb.gradle.task.FlywayRepairTask; +import org.flywaydb.gradle.task.FlywayUndoTask; +import org.flywaydb.gradle.task.FlywayValidateTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * Registers the plugin's tasks. + */ +public class FlywayPlugin implements Plugin { + public void apply(Project project) { + project.getExtensions().create("flyway", FlywayExtension.class); + project.getTasks().create("flywayClean", FlywayCleanTask.class); + project.getTasks().create("flywayBaseline", FlywayBaselineTask.class); + project.getTasks().create("flywayMigrate", FlywayMigrateTask.class); + project.getTasks().create("flywayUndo", FlywayUndoTask.class); + project.getTasks().create("flywayValidate", FlywayValidateTask.class); + project.getTasks().create("flywayInfo", FlywayInfoTask.class); + project.getTasks().create("flywayRepair", FlywayRepairTask.class); + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/AbstractFlywayTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/AbstractFlywayTask.java new file mode 100644 index 0000000..fd943d7 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/AbstractFlywayTask.java @@ -0,0 +1,920 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.gradle.task; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.internal.configuration.ConfigUtils; +import org.flywaydb.core.internal.jdbc.DriverDataSource; +import org.flywaydb.core.internal.util.StringUtils; +import org.flywaydb.gradle.FlywayExtension; +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetOutput; +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import static org.flywaydb.core.internal.configuration.ConfigUtils.putIfSet; + +/** + * A base class for all flyway tasks. + */ +@SuppressWarnings("WeakerAccess") +public abstract class AbstractFlywayTask extends DefaultTask { + /** + * The default Gradle configurations to use. + */ + // #2272: Gradle 4.x introduced additional configuration names and Gradle 5.0 deprecated some old ones. + // -> Rely on historic ones for Gradle 3.x + private static final String[] DEFAULT_CONFIGURATIONS_GRADLE3 = {"compileClasspath", "runtime", "testCompileClasspath", "testRuntime"}; + // -> And use new ones with Gradle 4.x and newer + private static final String[] DEFAULT_CONFIGURATIONS_GRADLE45 = {"compileClasspath", "runtimeClasspath", "testCompileClasspath", "testRuntimeClasspath"}; + + /** + * The flyway {} block in the build script. + */ + protected FlywayExtension extension; + + /** + * The fully qualified classname of the jdbc driver to use to connect to the database + */ + public String driver; + + /** + * The jdbc url to use to connect to the database + */ + public String url; + + /** + * The user to use to connect to the database + */ + public String user; + + /** + * The password to use to connect to the database + */ + public String 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. + * (default: 0) + *

Also configurable with Gradle or System Property: ${flyway.connectRetries}

+ */ + public int connectRetries; + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * (default: {@code null}) + *

Also configurable with Gradle or System Property: ${flyway.initSql}

+ */ + public String initSql; + + /** + *

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.

+ *

Also configurable with Gradle or System Property: ${flyway.table}

+ */ + public String table; + + /** + *

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.

+ *

Also configurable with Gradle or System Property: ${flyway.tablespace}

+ */ + public String tablespace; + + /** + * 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).
  • + *
+ *

Also configurable with Gradle or System Property: ${flyway.defaultSchema}

+ */ + public String defaultSchema; + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + */ + public Boolean createSchemas; + + /** + * 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.
  • + *
+ *

Also configurable with Gradle or System Property: ${flyway.schemas} (comma-separated list)

+ */ + public String[] schemas; + + /** + * The version to tag an existing schema with when executing baseline. (default: 1) + */ + public String baselineVersion; + + /** + * The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>) + */ + public String baselineDescription; + + /** + * 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.

+ * (default: filesystem:src/main/resources/db/migration) + */ + public String[] locations; + + /** + * The fully qualified class names of the custom MigrationResolvers to be used in addition (default) + * or as a replacement (using skipDefaultResolvers) to the built-in ones for resolving Migrations to + * apply. + *

(default: none)

+ */ + public String[] resolvers; + + /** + * If set to true, default built-in resolvers will be skipped, only custom migration resolvers will be used. + *

(default: false)

+ */ + public Boolean skipDefaultResolvers; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.sqlMigrationPrefix}

+ */ + public String sqlMigrationPrefix; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.undoSqlMigrationPrefix}

+ */ + public String undoSqlMigrationPrefix; + + /** + * 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

+ *

Also configurable with Gradle or System Property: ${flyway.repeatableSqlMigrationPrefix}

+ */ + public String repeatableSqlMigrationPrefix; + + /** + * 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

+ */ + public 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.

+ *

Also configurable with Gradle or System Property: ${flyway.sqlMigrationSuffixes}

+ */ + public String[] sqlMigrationSuffixes; + + /** + * The encoding of Sql migrations + */ + public String encoding; + + /** + * Placeholders to replace in Sql migrations + */ + public Map placeholders; + + /** + * Whether placeholders should be replaced. + */ + public Boolean placeholderReplacement; + + /** + * The prefix of every placeholder + */ + public String placeholderPrefix; + + /** + * The suffix of every placeholder + */ + public String placeholderSuffix; + + /** + * 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 String target; + + /** + * An array of fully qualified FlywayCallback class implementations + */ + public String[] callbacks; + + /** + * If set to true, default built-in callbacks will be skipped, only custom migration callbacks will be used. + *

(default: false)

+ */ + public Boolean skipDefaultCallbacks; + + /** + * Allows migrations to be run "out of order" + */ + public Boolean outOfOrder; + + /** + * Whether Flyway should output a table with the results of queries when executing migrations (default: true). + *

Flyway Pro and Flyway Enterprise only

+ *

Also configurable with Gradle or System Property: ${flyway.outputQueryResults}

+ */ + public Boolean outputQueryResults; + + /** + * Whether to automatically call validate or not when running migrate. (default: true) + */ + public Boolean validateOnMigrate; + + /** + * 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 !


+ *

Also configurable with Gradle or System Property: ${flyway.cleanOnValidationError}

+ */ + public Boolean cleanOnValidationError; + + /** + * 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.(default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreMissingMigrations}

+ */ + public 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.(default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreIgnoredMigrations}

+ */ + public Boolean 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. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.ignorePendingMigrations}

+ */ + public 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}) + *

Also configurable with Gradle or System Property: ${flyway.ignoreFutureMigrations}

+ */ + public Boolean 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. + *{@code false} to continue normally, {@code true} to fail fast with an exception. (default: {@code false}) + */ + public Boolean validateMigrationNaming; + + /** + * Whether to disable clean. (default: {@code false}) + *

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

+ */ + public Boolean cleanDisabled; + + /** + *

+ * 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! + *

+ *

{@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false})

+ */ + public Boolean baselineOnMigrate; + + /** + * 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.

+ *

{@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false})

+ */ + public 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})

+ */ + public 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}). + */ + public String installedBy; + + /** + * Gradle configurations that will be added to the classpath for running Flyway tasks. + * (default: compile, runtime, testCompile, testRuntime) + *

Also configurable with Gradle or System Property: ${flyway.configurations}

+ */ + public String[] configurations; + + /** + * 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}

+ *

Also configurable with Gradle or System Property: ${flyway.errorOverrides}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String[] errorOverrides; + + /** + * The file where to output the SQL statements of a migration dry run. If the file specified is in a non-existent + * directory, Flyway will create all directories and parent directories as needed. + *

{@code null} to execute the SQL statements directly against the database. (default: {@code null})

+ *

Also configurable with Gradle or System Property: ${flyway.dryRunOutput}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String dryRunOutput; + + /** + * 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. + * (default: {@code false} + *

Also configurable with Gradle or System Property: ${flyway.stream}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean stream; + + /** + * 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. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.batch}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean batch; + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + * (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.oracle.sqlplus}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean oracleSqlplus; + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. (default: {@code false}) + *

Also configurable with Gradle or System Property: ${flyway.oracle.sqlplusWarn}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public Boolean oracleSqlplusWarn; + + /** + * 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. + *

Also configurable with Gradle or System Property: ${flyway.licenseKey}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + public String licenseKey; + + /** + * The encoding of the external config files specified with the {@code flyway.configFiles} property. (default: UTF-8). + *

Also configurable with Gradle or System Property: ${flyway.configFileEncoding}

+ */ + public String configFileEncoding; + + /** + * Config files from which to load the Flyway configuration. The names of the individual properties match the ones you would + * use as Gradle or System properties. The encoding of the files is defined by the + * flyway.configFileEncoding property, which is UTF-8 by default. Relative paths are relative to the project root. + *

Also configurable with Gradle or System Property: ${flyway.configFiles}

+ */ + public String[] configFiles; + + /** + * The working directory to consider when dealing with relative paths for both config files and locations. + * (default: basedir, the directory where the POM resides) + *

+ *

Also configurable with Gradle or System Property: ${flyway.workingDirectory}

+ */ + public String workingDirectory; + + public AbstractFlywayTask() { + super(); + setGroup("Flyway"); + extension = (FlywayExtension) getProject().getExtensions().getByName("flyway"); + } + + @SuppressWarnings("unused") + @TaskAction + public Object runTask() { + try { + Map envVars = ConfigUtils.environmentVariablesToPropertyMap(); + + Set extraURLs = new HashSet<>(); + if (isJavaProject()) { + addClassesAndResourcesDirs(extraURLs); + addConfigurationArtifacts(determineConfigurations(envVars), extraURLs); + } + + ClassLoader classLoader = new URLClassLoader( + extraURLs.toArray(new URL[0]), + getProject().getBuildscript().getClassLoader()); + + Map config = createFlywayConfig(envVars); + ConfigUtils.dumpConfiguration(config); + + Flyway flyway = Flyway.configure(classLoader).configuration(config).load(); + Object result = run(flyway); + ((DriverDataSource) flyway.getConfiguration().getDataSource()).shutdownDatabase(); + return result; + } catch (Exception e) { + throw new FlywayException(collectMessages(e, "Error occurred while executing " + getName()), e); + } + } + + private void addClassesAndResourcesDirs(Set extraURLs) throws IllegalAccessException, InvocationTargetException, MalformedURLException { + JavaPluginConvention plugin = getProject().getConvention().getPlugin(JavaPluginConvention.class); + + for (SourceSet sourceSet : plugin.getSourceSets()) { + try { + @SuppressWarnings("JavaReflectionMemberAccess") + Method getClassesDirs = SourceSetOutput.class.getMethod("getClassesDirs"); + + // use alternative method available in Gradle 4.0 + FileCollection classesDirs = (FileCollection) getClassesDirs.invoke(sourceSet.getOutput()); + for (File directory : classesDirs.getFiles()) { + URL classesUrl = directory.toURI().toURL(); + getLogger().debug("Adding directory to Classpath: " + classesUrl); + extraURLs.add(classesUrl); + } + } catch (NoSuchMethodException e) { + // use original method available in Gradle 3.x + URL classesUrl = sourceSet.getOutput().getClassesDir().toURI().toURL(); + getLogger().debug("Adding directory to Classpath: " + classesUrl); + extraURLs.add(classesUrl); + } + + URL resourcesUrl = sourceSet.getOutput().getResourcesDir().toURI().toURL(); + getLogger().debug("Adding directory to Classpath: " + resourcesUrl); + extraURLs.add(resourcesUrl); + } + } + + private void addConfigurationArtifacts(String[] configurations, Set urls) throws IOException { + for (String configuration : configurations) { + getLogger().debug("Adding configuration to classpath: " + configuration); + ResolvedConfiguration resolvedConfiguration = + getProject().getConfigurations().getByName(configuration).getResolvedConfiguration(); + for (ResolvedArtifact artifact : resolvedConfiguration.getResolvedArtifacts()) { + URL artifactUrl = artifact.getFile().toURI().toURL(); + getLogger().debug("Adding artifact to classpath: " + artifactUrl); + urls.add(artifactUrl); + } + } + } + + private String[] determineConfigurations(Map envVars) { + if (envVars.containsKey(ConfigUtils.CONFIGURATIONS)) { + return StringUtils.tokenizeToStringArray(envVars.get(ConfigUtils.CONFIGURATIONS), ","); + } + if (System.getProperties().containsKey(ConfigUtils.CONFIGURATIONS)) { + return StringUtils.tokenizeToStringArray(System.getProperties().getProperty(ConfigUtils.CONFIGURATIONS), ","); + } + if (configurations != null) { + return configurations; + } + if (extension.configurations != null) { + return extension.configurations; + } + if (getProject().getGradle().getGradleVersion().startsWith("3")) { + return DEFAULT_CONFIGURATIONS_GRADLE3; + } + return DEFAULT_CONFIGURATIONS_GRADLE45; + } + + /** + * Executes the task's custom behavior. + * + * @param flyway The Flyway instance to use. + * @return The result of the task. + */ + protected abstract Object run(Flyway flyway); + + /** + * Creates the Flyway config to use. + */ + private Map createFlywayConfig(Map envVars) { + Map conf = new HashMap<>(); + + addLocationsToConfig(conf); + addConfigFromProperties(conf, loadConfigurationFromDefaultConfigFiles(envVars)); + + putIfSet(conf, ConfigUtils.DRIVER, driver, extension.driver); + putIfSet(conf, ConfigUtils.URL, url, extension.url); + putIfSet(conf, ConfigUtils.USER, user, extension.user); + putIfSet(conf, ConfigUtils.PASSWORD, password, extension.password); + putIfSet(conf, ConfigUtils.CONNECT_RETRIES, connectRetries, extension.connectRetries); + putIfSet(conf, ConfigUtils.INIT_SQL, initSql, extension.initSql); + putIfSet(conf, ConfigUtils.TABLE, table, extension.table); + putIfSet(conf, ConfigUtils.TABLESPACE, tablespace, extension.tablespace); + putIfSet(conf, ConfigUtils.BASELINE_VERSION, baselineVersion, extension.baselineVersion); + putIfSet(conf, ConfigUtils.BASELINE_DESCRIPTION, baselineDescription, extension.baselineDescription); + putIfSet(conf, ConfigUtils.SQL_MIGRATION_PREFIX, sqlMigrationPrefix, extension.sqlMigrationPrefix); + putIfSet(conf, ConfigUtils.UNDO_SQL_MIGRATION_PREFIX, undoSqlMigrationPrefix, extension.undoSqlMigrationPrefix); + putIfSet(conf, ConfigUtils.REPEATABLE_SQL_MIGRATION_PREFIX, repeatableSqlMigrationPrefix, extension.repeatableSqlMigrationPrefix); + putIfSet(conf, ConfigUtils.SQL_MIGRATION_SEPARATOR, sqlMigrationSeparator, extension.sqlMigrationSeparator); + putIfSet(conf, ConfigUtils.SQL_MIGRATION_SUFFIXES, StringUtils.arrayToCommaDelimitedString(sqlMigrationSuffixes), StringUtils.arrayToCommaDelimitedString(extension.sqlMigrationSuffixes)); + putIfSet(conf, ConfigUtils.MIXED, mixed, extension.mixed); + putIfSet(conf, ConfigUtils.GROUP, group, extension.group); + putIfSet(conf, ConfigUtils.INSTALLED_BY, installedBy, extension.installedBy); + putIfSet(conf, ConfigUtils.ENCODING, encoding, extension.encoding); + putIfSet(conf, ConfigUtils.PLACEHOLDER_REPLACEMENT, placeholderReplacement, extension.placeholderReplacement); + putIfSet(conf, ConfigUtils.PLACEHOLDER_PREFIX, placeholderPrefix, extension.placeholderPrefix); + putIfSet(conf, ConfigUtils.PLACEHOLDER_SUFFIX, placeholderSuffix, extension.placeholderSuffix); + putIfSet(conf, ConfigUtils.TARGET, target, extension.target); + putIfSet(conf, ConfigUtils.OUT_OF_ORDER, outOfOrder, extension.outOfOrder); + putIfSet(conf, ConfigUtils.OUTPUT_QUERY_RESULTS, outputQueryResults, extension.outputQueryResults); + putIfSet(conf, ConfigUtils.VALIDATE_ON_MIGRATE, validateOnMigrate, extension.validateOnMigrate); + putIfSet(conf, ConfigUtils.CLEAN_ON_VALIDATION_ERROR, cleanOnValidationError, extension.cleanOnValidationError); + putIfSet(conf, ConfigUtils.IGNORE_MISSING_MIGRATIONS, ignoreMissingMigrations, extension.ignoreMissingMigrations); + putIfSet(conf, ConfigUtils.IGNORE_IGNORED_MIGRATIONS, ignoreIgnoredMigrations, extension.ignoreIgnoredMigrations); + putIfSet(conf, ConfigUtils.IGNORE_PENDING_MIGRATIONS, ignorePendingMigrations, extension.ignorePendingMigrations); + putIfSet(conf, ConfigUtils.IGNORE_FUTURE_MIGRATIONS, ignoreFutureMigrations, extension.ignoreFutureMigrations); + putIfSet(conf, ConfigUtils.VALIDATE_MIGRATION_NAMING, validateMigrationNaming, extension.validateMigrationNaming); + putIfSet(conf, ConfigUtils.CLEAN_DISABLED, cleanDisabled, extension.cleanDisabled); + putIfSet(conf, ConfigUtils.BASELINE_ON_MIGRATE, baselineOnMigrate, extension.baselineOnMigrate); + putIfSet(conf, ConfigUtils.SKIP_DEFAULT_RESOLVERS, skipDefaultResolvers, extension.skipDefaultResolvers); + putIfSet(conf, ConfigUtils.SKIP_DEFAULT_CALLBACKS, skipDefaultCallbacks, extension.skipDefaultCallbacks); + putIfSet(conf, ConfigUtils.DEFAULT_SCHEMA, defaultSchema, extension.defaultSchema); + putIfSet(conf, ConfigUtils.CREATE_SCHEMAS, createSchemas, extension.createSchemas); + + putIfSet(conf, ConfigUtils.SCHEMAS, StringUtils.arrayToCommaDelimitedString(schemas), StringUtils.arrayToCommaDelimitedString(extension.schemas)); + putIfSet(conf, ConfigUtils.RESOLVERS, StringUtils.arrayToCommaDelimitedString(resolvers), StringUtils.arrayToCommaDelimitedString(extension.resolvers)); + putIfSet(conf, ConfigUtils.CALLBACKS, StringUtils.arrayToCommaDelimitedString(callbacks), StringUtils.arrayToCommaDelimitedString(extension.callbacks)); + putIfSet(conf, ConfigUtils.ERROR_OVERRIDES, StringUtils.arrayToCommaDelimitedString(errorOverrides), StringUtils.arrayToCommaDelimitedString(extension.errorOverrides)); + + putIfSet(conf, ConfigUtils.DRYRUN_OUTPUT, dryRunOutput, extension.dryRunOutput); + putIfSet(conf, ConfigUtils.STREAM, stream, extension.stream); + putIfSet(conf, ConfigUtils.BATCH, batch, extension.batch); + + putIfSet(conf, ConfigUtils.ORACLE_SQLPLUS, oracleSqlplus, extension.oracleSqlplus); + putIfSet(conf, ConfigUtils.ORACLE_SQLPLUS_WARN, oracleSqlplusWarn, extension.oracleSqlplusWarn); + + putIfSet(conf, ConfigUtils.LICENSE_KEY, licenseKey, extension.licenseKey); + + if (placeholders != null) { + for (Map.Entry entry : placeholders.entrySet()) { + conf.put(ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX + entry.getKey().toString(), entry.getValue().toString()); + } + } + if (extension.placeholders != null) { + for (Map.Entry entry : extension.placeholders.entrySet()) { + conf.put(ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX + entry.getKey().toString(), entry.getValue().toString()); + } + } + + addConfigFromProperties(conf, getProject().getProperties()); + addConfigFromProperties(conf, loadConfigurationFromConfigFiles(getWorkingDirectory(), envVars)); + addConfigFromProperties(conf, envVars); + addConfigFromProperties(conf, System.getProperties()); + removeGradlePluginSpecificPropertiesToAvoidWarnings(conf); + + return conf; + } + + private void addLocationsToConfig(Map conf) { + File workingDirectory = getWorkingDirectory(); + + conf.put(ConfigUtils.LOCATIONS, Location.FILESYSTEM_PREFIX + workingDirectory + "/src/main/resources/db/migration"); + + String[] locationsToAdd = getLocations(); + + if (locationsToAdd != null) { + for (int i = 0; i < locationsToAdd.length; i++) { + if (locationsToAdd[i].startsWith(Location.FILESYSTEM_PREFIX)) { + String newLocation = locationsToAdd[i].substring(Location.FILESYSTEM_PREFIX.length()); + File file = new File(newLocation); + if (!file.isAbsolute()) { + file = new File(workingDirectory, newLocation); + } + locationsToAdd[i] = Location.FILESYSTEM_PREFIX + file.getAbsolutePath(); + } + } + } + + putIfSet(conf, ConfigUtils.LOCATIONS, StringUtils.arrayToCommaDelimitedString(locationsToAdd)); + } + + private String[] getLocations() { + // To maintain override order, return extension value first if present + if (extension.locations != null) { + return extension.locations; + } + + if (locations != null) { + return locations; + } + + return null; + } + + private File getWorkingDirectory() { + // To maintain override order, return extension value first if present + if (extension.workingDirectory != null) { + return new File(extension.workingDirectory); + } + + if (workingDirectory != null) { + return new File(workingDirectory); + } + + return new File(getProject().getProjectDir().getAbsolutePath()); + } + + /** + * Retrieve the properties from the config files (if specified). + * + * @param workingDirectory The working directory to use. + * @param envVars The environment variables converted to Flyway properties. + * @return The properties. + */ + private Map loadConfigurationFromConfigFiles(File workingDirectory, Map envVars) { + String encoding = determineConfigurationFileEncoding(envVars); + + Map conf = new HashMap<>(); + for (File configFile : determineConfigFiles(workingDirectory, envVars)) { + conf.putAll(ConfigUtils.loadConfigurationFile(configFile, encoding, true)); + } + return conf; + } + + /** + * Retrieve the properties from the config files (if specified). + * + * @param envVars The environment variables converted to Flyway properties. + * @return The properties. + */ + private Map loadConfigurationFromDefaultConfigFiles(Map envVars) { + String encoding = determineConfigurationFileEncoding(envVars); + File configFile = new File(System.getProperty("user.home") + "/" + ConfigUtils.CONFIG_FILE_NAME); + + return new HashMap<>(ConfigUtils.loadConfigurationFile(configFile, encoding, false)); + } + + /** + * Determines the encoding to use for loading the configuration files. + * + * @param envVars The environment variables converted to Flyway properties. + * @return The encoding. (default: UTF-8) + */ + private String determineConfigurationFileEncoding(Map envVars) { + if (envVars.containsKey(ConfigUtils.CONFIG_FILE_ENCODING)) { + return envVars.get(ConfigUtils.CONFIG_FILE_ENCODING); + } + if (System.getProperties().containsKey(ConfigUtils.CONFIG_FILE_ENCODING)) { + return System.getProperties().getProperty(ConfigUtils.CONFIG_FILE_ENCODING); + } + if (configFileEncoding != null) { + return configFileEncoding; + } + if (extension.configFileEncoding != null) { + return extension.configFileEncoding; + } + return "UTF-8"; + } + + /** + * Determines the files to use for loading the configuration. + * + * @param workingDirectory The working directory to use. + * @param envVars The environment variables converted to Flyway properties. + * @return The configuration files. + */ + private List determineConfigFiles(File workingDirectory, Map envVars) { + List configFiles = new ArrayList<>(); + + if (envVars.containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(envVars.get(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(toFile(workingDirectory, file)); + } + return configFiles; + } + + if (System.getProperties().containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(System.getProperties().getProperty(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(toFile(workingDirectory, file)); + } + return configFiles; + } + + if (getProject().getProperties().containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(String.valueOf(getProject().getProperties().get(ConfigUtils.CONFIG_FILES)), ",")) { + configFiles.add(toFile(workingDirectory, file)); + } + return configFiles; + } + + if (this.configFiles != null) { + for (String file : this.configFiles) { + configFiles.add(toFile(workingDirectory, file)); + } + return configFiles; + } + + if (extension.configFiles != null) { + for (String file : extension.configFiles) { + configFiles.add(toFile(workingDirectory, file)); + } + return configFiles; + } + + return configFiles; + } + + /** + * Converts this fileName into a file, adjusting relative paths if necessary to make them relative to the pom. + * + * @param workingDirectory The working directory to use. + * @param fileName The name of the file, relative or absolute. + * @return The resulting file. + */ + private File toFile(File workingDirectory, String fileName) { + File file = new File(fileName); + if (file.isAbsolute()) { + return file; + } + return new File(workingDirectory, fileName); + } + + /** + * Filters there properties to remove the Flyway Gradle Plugin-specific ones to avoid warnings. + * + * @param conf The properties to filter. + */ + private static void removeGradlePluginSpecificPropertiesToAvoidWarnings(Map conf) { + conf.remove(ConfigUtils.CONFIG_FILES); + conf.remove(ConfigUtils.CONFIG_FILE_ENCODING); + conf.remove(ConfigUtils.CONFIGURATIONS); + conf.remove("flyway.version"); + conf.remove("flyway.workingDirectory"); + } + + private static void addConfigFromProperties(Map config, Properties properties) { + for (String prop : properties.stringPropertyNames()) { + if (prop.startsWith("flyway.")) { + config.put(prop, properties.getProperty(prop)); + } + } + } + + private static void addConfigFromProperties(Map config, Map properties) { + for (String prop : properties.keySet()) { + if (prop.startsWith("flyway.")) { + config.put(prop, properties.get(prop).toString()); + } + } + } + + /** + * Collect error messages from the stack trace + * + * @param throwable Throwable instance from which the message should be build + * @param message the message to which the error message will be appended + * @return a String containing the composed messages + */ + private String collectMessages(Throwable throwable, String message) { + if (throwable != null) { + message += "\n" + throwable.getMessage(); + return collectMessages(throwable.getCause(), message); + } + return message; + } + + private boolean isJavaProject() { + return getProject().getPluginManager().hasPlugin("java"); + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayBaselineTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayBaselineTask.java new file mode 100644 index 0000000..4a57823 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayBaselineTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +public class FlywayBaselineTask extends AbstractFlywayTask { + public FlywayBaselineTask() { + super(); + setDescription("Baselines an existing database, excluding all migrations up to and including baselineVersion."); + } + + @Override + protected Object run(Flyway flyway) { + flyway.baseline(); + return null; + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayCleanTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayCleanTask.java new file mode 100644 index 0000000..67be0a4 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayCleanTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +public class FlywayCleanTask extends AbstractFlywayTask { + public FlywayCleanTask() { + super(); + setDescription("Drops all objects in the configured schemas."); + } + + @Override + protected Object run(Flyway flyway) { + flyway.clean(); + return null; + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayInfoTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayInfoTask.java new file mode 100644 index 0000000..a8251e4 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayInfoTask.java @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.gradle.task; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.internal.info.MigrationInfoDumper; + +public class FlywayInfoTask extends AbstractFlywayTask { + public FlywayInfoTask() { + super(); + setDescription("Prints the details and status information about all the migrations."); + } + + @Override + protected Object run(Flyway flyway) { + MigrationInfoService info = flyway.info(); + MigrationInfo current = info.current(); + MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion(); + System.out.println("Schema version: " + currentSchemaVersion); + System.out.println(MigrationInfoDumper.dumpToAsciiTable(info.all())); + return null; + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayMigrateTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayMigrateTask.java new file mode 100644 index 0000000..82ec261 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayMigrateTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +public class FlywayMigrateTask extends AbstractFlywayTask { + public FlywayMigrateTask() { + super(); + setDescription("Migrates the schema to the latest version."); + } + + @Override + protected Object run(Flyway flyway) { + return flyway.migrate(); + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayRepairTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayRepairTask.java new file mode 100644 index 0000000..27f1491 --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayRepairTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +/** + * 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
  • + *
+ */ +public class FlywayRepairTask extends AbstractFlywayTask { + public FlywayRepairTask() { + super(); + setDescription("Repairs the Flyway schema history table."); + } + + @Override + protected Object run(Flyway flyway) { + flyway.repair(); + return null; + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayUndoTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayUndoTask.java new file mode 100644 index 0000000..a1f5b2c --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayUndoTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +public class FlywayUndoTask extends AbstractFlywayTask { + public FlywayUndoTask() { + super(); + setDescription("Undoes the most recently applied versioned migration. Flyway Pro and Flyway Enterprise only."); + } + + @Override + protected Object run(Flyway flyway) { + return flyway.undo(); + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayValidateTask.java a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayValidateTask.java new file mode 100644 index 0000000..93b199c --- /dev/null +++ a/flyway-gradle-plugin/src/main/java/org/flywaydb/gradle/task/FlywayValidateTask.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.gradle.task; + +import org.flywaydb.core.Flyway; + +/** + *

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 + */ +public class FlywayValidateTask extends AbstractFlywayTask { + public FlywayValidateTask() { + super(); + setDescription("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 or" + + " versions have been resolved that haven\"t been applied yet"); + } + + @Override + protected Object run(Flyway flyway) { + flyway.validate(); + return null; + } +} \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.enterprise.flyway.properties a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.enterprise.flyway.properties new file mode 100644 index 0000000..ae0e23f --- /dev/null +++ a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.enterprise.flyway.properties @@ -0,0 +1,17 @@ +# +# Copyright 2010-2020 Redgate Software Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.flywaydb.gradle.FlywayPlugin \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.flyway.properties a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.flyway.properties new file mode 100644 index 0000000..ae0e23f --- /dev/null +++ a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.flyway.properties @@ -0,0 +1,17 @@ +# +# Copyright 2010-2020 Redgate Software Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.flywaydb.gradle.FlywayPlugin \ No newline at end of file diff --git b/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.pro.flyway.properties a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.pro.flyway.properties new file mode 100644 index 0000000..ae0e23f --- /dev/null +++ a/flyway-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.flywaydb.pro.flyway.properties @@ -0,0 +1,17 @@ +# +# Copyright 2010-2020 Redgate Software Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.flywaydb.gradle.FlywayPlugin \ No newline at end of file diff --git b/flyway-maven-plugin/pom.xml a/flyway-maven-plugin/pom.xml new file mode 100644 index 0000000..2e543a2 --- /dev/null +++ a/flyway-maven-plugin/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + + org.flywaydb + flyway-parent + 0-SNAPSHOT + + flyway-maven-plugin + maven-plugin + ${project.artifactId} + + + org.apache.maven + maven-plugin-api + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + provided + + + org.apache.maven + maven-artifact + + + org.apache.maven + maven-model + + + org.apache.maven + maven-core + + + org.sonatype.plexus + plexus-sec-dispatcher + 1.4 + + + org.codehaus.plexus + plexus-utils + 1.5.15 + + + ${project.groupId} + flyway-core + ${project.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + maven-resources-plugin + + + copy-license + + copy-resources + + generate-resources + + + + .. + + LICENSE.txt + README.txt + + + + ${project.build.outputDirectory}/META-INF + + + + + + maven-plugin-plugin + + UTF-8 + + + + default-descriptor + process-classes + + + help-goal + + helpmojo + + process-classes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/AbstractFlywayMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/AbstractFlywayMojo.java new file mode 100644 index 0000000..f0309eb --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/AbstractFlywayMojo.java @@ -0,0 +1,926 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.maven; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; +import org.codehaus.plexus.classworlds.realm.ClassRealm; +import org.flywaydb.core.Flyway; +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.configuration.ConfigUtils; +import org.flywaydb.core.internal.util.ExceptionUtils; +import org.flywaydb.core.internal.util.StringUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.flywaydb.core.internal.configuration.ConfigUtils.putArrayIfSet; +import static org.flywaydb.core.internal.configuration.ConfigUtils.putIfSet; + +/** + * Common base class for all mojos with all common attributes. + */ +@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) +abstract class AbstractFlywayMojo extends AbstractMojo { + private static final String CONFIG_WORKING_DIRECTORY = "flyway.workingDirectory"; + private static final String CONFIG_SERVER_ID = "flyway.serverId"; + private static final String CONFIG_VERSION = "flyway.version"; + private static final String CONFIG_SKIP = "flyway.skip"; + private static final String CONFIG_CURRENT = "flyway.current"; + + Log log; + + /** + * Whether to skip the execution of the Maven Plugin for this module. + *

Also configurable with Maven or System Property: ${flyway.skip}

+ */ + @Parameter(property = CONFIG_SKIP) + /* private -> for testing */ boolean skip; + + /** + * The fully qualified classname of the jdbc driver to use to connect to the database.
+ * By default, the driver is autodetected based on the url.
+ *

Also configurable with Maven or System Property: ${flyway.driver}

+ */ + @Parameter(property = ConfigUtils.DRIVER) + /* private -> for testing */ String driver; + + /** + * The jdbc url to use to connect to the database.
+ *

Also configurable with Maven or System Property: ${flyway.url}

+ */ + @Parameter(property = ConfigUtils.URL) + /* private -> for testing */ String url; + + /** + * The user to use to connect to the database. (default: blank)
+ * The credentials can be specified by user/password or {@code serverId} from settings.xml + *

Also configurable with Maven or System Property: ${flyway.user}

+ */ + @Parameter(property = ConfigUtils.USER) + /* private -> for testing */ String user; + + /** + * The password to use to connect to the database. (default: blank)
+ *

Also configurable with Maven or System Property: ${flyway.password}

+ */ + @Parameter(property = ConfigUtils.PASSWORD) + private String 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. + * (default: 0) + *

Also configurable with Maven or System Property: ${flyway.connectRetries}

+ */ + @Parameter(property = ConfigUtils.CONNECT_RETRIES) + private int connectRetries; + + /** + * The SQL statements to run to initialize a new database connection immediately after opening it. + * (default: {@code null}) + *

Also configurable with Maven or System Property: ${flyway.initSql}

+ */ + @Parameter(property = ConfigUtils.INIT_SQL) + private String initSql; + + /** + * 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).
  • + *
+ *

Also configurable with Maven or System Property: ${flyway.defaultSchema}

+ */ + @Parameter + private String defaultSchema; + + /** + * 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.
  • + *
+ *

Also configurable with Maven or System Property: ${flyway.schemas} (comma-separated list)

+ */ + @Parameter + private String[] schemas; + + /** + *

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 {@code flyway.schemas} property is set (multi-schema mode), the + * schema history table is placed in the first schema of the list.

+ *

Also configurable with Maven or System Property: ${flyway.table}

+ */ + @Parameter(property = ConfigUtils.TABLE) + private String table; + + /** + *

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.

+ *

Also configurable with Maven or System Property: ${flyway.tablespace}

+ */ + @Parameter(property = ConfigUtils.TABLESPACE) + private String tablespace; + + /** + * The version to tag an existing schema with when executing baseline. (default: 1)
+ *

Also configurable with Maven or System Property: ${flyway.baselineVersion}

+ */ + @Parameter(property = ConfigUtils.BASELINE_VERSION) + private String baselineVersion; + + /** + * The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>)
+ *

Also configurable with Maven or System Property: ${flyway.baselineDescription}

+ */ + @Parameter(property = ConfigUtils.BASELINE_DESCRIPTION) + private String baselineDescription; + + /** + * 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.

+ * (default: filesystem:src/main/resources/db/migration) + *

Also configurable with Maven or System Property: ${flyway.locations} (Comma-separated list)

+ */ + @Parameter + private String[] locations; + + /** + * The fully qualified class names of the custom MigrationResolvers to be used in addition or as replacement + * (if skipDefaultResolvers is true) to the built-in ones for resolving Migrations to apply. + *

(default: none)

+ *

Also configurable with Maven or System Property: ${flyway.resolvers} (Comma-separated list)

+ */ + @Parameter + private String[] resolvers; + + /** + * When set to true, default resolvers are skipped, i.e. only custom resolvers as defined by 'resolvers' + * are used. (default: false)

Also configurable with Maven or System Property: + * ${flyway.skipDefaultResolvers}

+ */ + @Parameter(property = ConfigUtils.SKIP_DEFAULT_RESOLVERS) + private Boolean skipDefaultResolvers; + + /** + * The encoding of Sql migrations. (default: UTF-8)

Also configurable with Maven or System Property: + * ${flyway.encoding}

+ */ + @Parameter(property = ConfigUtils.ENCODING) + private String encoding; + + /** + * 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

+ *

Also configurable with Maven or System Property: ${flyway.sqlMigrationPrefix}

+ */ + @Parameter(property = ConfigUtils.SQL_MIGRATION_PREFIX) + private String sqlMigrationPrefix; + + /** + * 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

+ *

Also configurable with Maven or System Property: ${flyway.undoSqlMigrationPrefix}

+ */ + @Parameter(property = ConfigUtils.UNDO_SQL_MIGRATION_PREFIX) + private String undoSqlMigrationPrefix; + + /** + * The file name prefix for repeatable sql migrations (default: R)

Also configurable with Maven or System Property: + * ${flyway.repeatableSqlMigrationPrefix}

+ *

+ *

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

+ */ + @Parameter(property = ConfigUtils.REPEATABLE_SQL_MIGRATION_PREFIX) + private String repeatableSqlMigrationPrefix; + + /** + * The file name separator for Sql migrations (default: __)

Also configurable with Maven or System Property: + * ${flyway.sqlMigrationSeparator}

+ *

+ *

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

+ */ + @Parameter(property = ConfigUtils.SQL_MIGRATION_SEPARATOR) + 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.

+ *

Also configurable with Maven or System Property: ${flyway.sqlMigrationSuffixes}

+ */ + @Parameter + private String[] sqlMigrationSuffixes; + + /** + * 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 !


+ *

Also configurable with Maven or System Property: ${flyway.cleanOnValidationError}

+ */ + @Parameter(property = ConfigUtils.CLEAN_ON_VALIDATION_ERROR) + 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.

+ *

Also configurable with Maven or System Property: ${flyway.cleanDisabled}

+ */ + @Parameter(property = ConfigUtils.CLEAN_DISABLED) + private Boolean cleanDisabled; + + /** + * 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}. + *

Also configurable with Maven or System Property: ${flyway.target}

+ */ + @Parameter(property = ConfigUtils.TARGET) + private String target; + + /** + * Allows migrations to be run "out of order" (default: {@code false}). + *

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.

+ *

Also configurable with Maven or System Property: ${flyway.outOfOrder}

+ */ + @Parameter(property = ConfigUtils.OUT_OF_ORDER) + private Boolean outOfOrder; + + /** + * Whether Flyway should output a table with the results of queries when executing migrations (default: true). + *

Flyway Pro and Flyway Enterprise only

+ *

Also configurable with Maven or System Property: ${flyway.outputQueryResults}

+ */ + @Parameter(property = ConfigUtils.OUTPUT_QUERY_RESULTS) + private Boolean outputQueryResults; + + /** + * 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. (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.ignoreMissingMigrations}

+ */ + @Parameter(property = ConfigUtils.IGNORE_MISSING_MIGRATIONS) + 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. (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.ignoreIgnoredMigrations}

+ */ + @Parameter(property = ConfigUtils.IGNORE_IGNORED_MIGRATIONS) + private Boolean 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. + * (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.ignorePendingMigrations}

+ */ + @Parameter(property = ConfigUtils.IGNORE_PENDING_MIGRATIONS) + 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}) + *

Also configurable with Maven or System Property: ${flyway.ignoreFutureMigrations}

+ */ + @Parameter(property = ConfigUtils.IGNORE_FUTURE_MIGRATIONS) + private Boolean 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. + *

Also configurable with Maven or System Property: ${flyway.validateMigrationNaming}

+ */ + @Parameter(property = ConfigUtils.VALIDATE_MIGRATION_NAMING) + private Boolean validateMigrationNaming; + + /** + * Whether placeholders should be replaced. (default: true)
+ *

Also configurable with Maven or System Property: ${flyway.placeholderReplacement}

+ */ + @Parameter(property = ConfigUtils.PLACEHOLDER_REPLACEMENT) + private Boolean placeholderReplacement; + + /** + * A map of <placeholder, replacementValue> to apply to sql migration scripts. + *

+ *

Also configurable with Maven or System Properties like ${flyway.placeholders.myplaceholder} or ${flyway.placeholders.otherone}

+ */ + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + @Parameter + private Map placeholders; + + /** + * The prefix of every placeholder. (default: ${ )
+ *

Also configurable with Maven or System Property: ${flyway.placeholderPrefix}

+ */ + @Parameter(property = ConfigUtils.PLACEHOLDER_PREFIX) + private String placeholderPrefix; + + /** + * The suffix of every placeholder. (default: } )
+ *

Also configurable with Maven or System Property: ${flyway.placeholderSuffix}

+ */ + @Parameter(property = ConfigUtils.PLACEHOLDER_SUFFIX) + private String placeholderSuffix; + + /** + * An array of FlywayCallback implementations. (default: empty )
+ *

Also configurable with Maven or System Property: ${flyway.callbacks}

+ */ + @Parameter + private String[] callbacks; + + /** + * When set to true, default callbacks are skipped, i.e. only custom callbacks as defined by 'resolvers' + * are used. (default: false)

Also configurable with Maven or System Property: + * ${flyway.skipDefaultCallbacks}

+ */ + @Parameter(property = ConfigUtils.SKIP_DEFAULT_CALLBACKS) + private Boolean skipDefaultCallbacks; + + /** + *

+ * 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 initialVersion} before executing the migrations. + * Only migrations above {@code initialVersion} 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}) + *

+ *

Also configurable with Maven or System Property: ${flyway.baselineOnMigrate}

+ */ + @Parameter(property = ConfigUtils.BASELINE_ON_MIGRATE) + private Boolean baselineOnMigrate; + + /** + * Whether to automatically call validate or not when running migrate. (default: {@code true})
+ *

Also configurable with Maven or System Property: ${flyway.validateOnMigrate}

+ */ + @Parameter(property = ConfigUtils.VALIDATE_ON_MIGRATE) + private Boolean validateOnMigrate; + + /** + * 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.

+ * {@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.mixed}

+ */ + @Parameter(property = ConfigUtils.MIXED) + 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})

+ *

Also configurable with Maven or System Property: ${flyway.group}

+ */ + @Parameter(property = ConfigUtils.GROUP) + 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}).

+ *

Also configurable with Maven or System Property: ${flyway.installedBy}

+ */ + @Parameter(property = ConfigUtils.INSTALLED_BY) + private String installedBy; + + /** + * 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}

+ *

Also configurable with Maven or System Property: ${flyway.errorOverrides}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter + private String[] errorOverrides; + + /** + * The file where to output the SQL statements of a migration dry run. If the file specified is in a non-existent + * directory, Flyway will create all directories and parent directories as needed. + *

{@code null} to execute the SQL statements directly against the database. (default: {@code null})

+ *

Also configurable with Maven or System Property: ${flyway.dryRunOutput}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.DRYRUN_OUTPUT) + private String dryRunOutput; + + /** + * 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. + * (default: {@code false} + *

Also configurable with Maven or System Property: ${flyway.stream}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.STREAM) + private Boolean stream; + + /** + * 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. + * (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.batch}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.BATCH) + private Boolean batch; + + /** + * Whether to Flyway's support for Oracle SQL*Plus commands should be activated. + * (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.oracle.sqlplus}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.ORACLE_SQLPLUS) + private Boolean oracleSqlplus; + + /** + * Whether Flyway should issue a warning instead of an error whenever it encounters an Oracle SQL*Plus statement + * it doesn't yet support. (default: {@code false}) + *

Also configurable with Maven or System Property: ${flyway.oracle.sqlplusWarn}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.ORACLE_SQLPLUS_WARN) + private Boolean oracleSqlplusWarn; + + /** + * 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. + *

Also configurable with Maven or System Property: ${flyway.licenseKey}

+ *

Flyway Pro and Flyway Enterprise only

+ */ + @Parameter(property = ConfigUtils.LICENSE_KEY) + private String licenseKey; + + /** + * The encoding of the external config files specified with the {@code flyway.configFiles} property. (default: UTF-8). + *

+ *

Also configurable with Maven or System Property: ${flyway.configFileEncoding}

+ */ + @Parameter(property = ConfigUtils.CONFIG_FILE_ENCODING) + private String configFileEncoding; + + /** + * Config files from which to load the Flyway configuration. The names of the individual properties match the ones you would + * use as Maven or System properties. The encoding of the files is defined by the + * flyway.configFileEncoding property, which is UTF-8 by default. Relative paths are relative to the POM or + * workingDirectory if set. + *

+ *

Also configurable with Maven or System Property: ${flyway.configFiles}

+ */ + @Parameter(property = ConfigUtils.CONFIG_FILES) + private File[] configFiles; + + /** + * Whether Flyway should attempt to create the schemas specified in the schemas property + * + *

Also configurable with Maven or System Property: ${flyway.createSchemas}

+ */ + @Parameter(property = ConfigUtils.CREATE_SCHEMAS) + private Boolean createSchemas; + + /** + * The working directory to consider when dealing with relative paths for both config files and locations. + * (default: basedir, the directory where the POM resides) + *

+ *

Also configurable with Maven or System Property: ${flyway.workingDirectory}

+ */ + @Parameter(property = CONFIG_WORKING_DIRECTORY) + private File workingDirectory; + + /** + * The id of the server tag in settings.xml (default: flyway-db)
+ * The credentials can be specified by user/password or {@code serverId} from settings.xml
+ *

Also configurable with Maven or System Property: ${flyway.serverId}

+ */ + @Parameter(property = CONFIG_SERVER_ID) + private String serverId = "flyway-db"; + + /** + * The link to the settings.xml + */ + @Parameter(defaultValue = "${settings}", readonly = true) + /* private -> for testing */ Settings settings; + + /** + * Reference to the current project that includes the Flyway Maven plugin. + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + /* private -> for testing */ MavenProject mavenProject; + + @Component + private SettingsDecrypter settingsDecrypter; + + /** + * Load username password from settings + * + * @throws FlywayException when the credentials could not be loaded. + */ + private void loadCredentialsFromSettings() throws FlywayException { + final Server server = settings.getServer(serverId); + if (user == null) { + if (server != null) { + user = server.getUsername(); + SettingsDecryptionResult result = + settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server)); + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new FlywayException("Unable to decrypt password: " + problem, problem.getException()); + } + } + password = result.getServer().getPassword(); + } + } else if (server != null) { + throw new FlywayException("You specified credentials both in the Flyway config and settings.xml. Use either one or the other"); + } + } + + /** + * Retrieves the value of this boolean property, based on the matching System on the Maven property. + * + * @param systemPropertyName The name of the System property. + * @param mavenPropertyValue The value of the Maven property. + * @return The value to use. + */ + /* private -> for testing */ boolean getBooleanProperty(String systemPropertyName, boolean mavenPropertyValue) { + String systemPropertyValue = System.getProperty(systemPropertyName); + if (systemPropertyValue != null) { + return Boolean.parseBoolean(systemPropertyValue); + } + return mavenPropertyValue; + } + + public final void execute() throws MojoExecutionException { + LogFactory.setLogCreator(new MavenLogCreator(this)); + log = LogFactory.getLog(getClass()); + + if (getBooleanProperty(CONFIG_SKIP, skip)) { + log.info("Skipping Flyway execution"); + return; + } + + try { + Set classpathElements = new HashSet<>(); + classpathElements.addAll(mavenProject.getCompileClasspathElements()); + classpathElements.addAll(mavenProject.getRuntimeClasspathElements()); + + ClassRealm classLoader = (ClassRealm) Thread.currentThread().getContextClassLoader(); + for (String classpathElement : classpathElements) { + classLoader.addURL(new File(classpathElement).toURI().toURL()); + } + + File workDir = workingDirectory == null ? mavenProject.getBasedir() : workingDirectory; + + if (locations != null) { + for (int i = 0; i < locations.length; i++) { + if (locations[i].startsWith(Location.FILESYSTEM_PREFIX)) { + String newLocation = locations[i].substring(Location.FILESYSTEM_PREFIX.length()); + File file = new File(newLocation); + if (!file.isAbsolute()) { + file = new File(workDir, newLocation); + } + locations[i] = Location.FILESYSTEM_PREFIX + file.getAbsolutePath(); + } + } + } else { + locations = new String[]{ + Location.FILESYSTEM_PREFIX + workDir.getAbsolutePath() + "/src/main/resources/db/migration" + }; + } + + Map envVars = ConfigUtils.environmentVariablesToPropertyMap(); + + Map conf = new HashMap<>(loadConfigurationFromDefaultConfigFiles(envVars)); + + loadCredentialsFromSettings(); + + putIfSet(conf, ConfigUtils.DRIVER, driver); + putIfSet(conf, ConfigUtils.URL, url); + putIfSet(conf, ConfigUtils.USER, user); + putIfSet(conf, ConfigUtils.PASSWORD, password); + putIfSet(conf, ConfigUtils.CONNECT_RETRIES, connectRetries); + putIfSet(conf, ConfigUtils.INIT_SQL, initSql); + putIfSet(conf, ConfigUtils.DEFAULT_SCHEMA, defaultSchema); + putArrayIfSet(conf, ConfigUtils.SCHEMAS, schemas); + putIfSet(conf, ConfigUtils.TABLE, table); + putIfSet(conf, ConfigUtils.TABLESPACE, tablespace); + putIfSet(conf, ConfigUtils.BASELINE_VERSION, baselineVersion); + putIfSet(conf, ConfigUtils.BASELINE_DESCRIPTION, baselineDescription); + putArrayIfSet(conf, ConfigUtils.LOCATIONS, locations); + putArrayIfSet(conf, ConfigUtils.RESOLVERS, resolvers); + putIfSet(conf, ConfigUtils.SKIP_DEFAULT_RESOLVERS, skipDefaultResolvers); + putArrayIfSet(conf, ConfigUtils.CALLBACKS, callbacks); + putIfSet(conf, ConfigUtils.SKIP_DEFAULT_CALLBACKS, skipDefaultCallbacks); + putIfSet(conf, ConfigUtils.ENCODING, encoding); + putIfSet(conf, ConfigUtils.SQL_MIGRATION_PREFIX, sqlMigrationPrefix); + putIfSet(conf, ConfigUtils.UNDO_SQL_MIGRATION_PREFIX, undoSqlMigrationPrefix); + putIfSet(conf, ConfigUtils.REPEATABLE_SQL_MIGRATION_PREFIX, repeatableSqlMigrationPrefix); + putIfSet(conf, ConfigUtils.SQL_MIGRATION_SEPARATOR, sqlMigrationSeparator); + putArrayIfSet(conf, ConfigUtils.SQL_MIGRATION_SUFFIXES, sqlMigrationSuffixes); + putIfSet(conf, ConfigUtils.MIXED, mixed); + putIfSet(conf, ConfigUtils.GROUP, group); + putIfSet(conf, ConfigUtils.INSTALLED_BY, installedBy); + putIfSet(conf, ConfigUtils.CLEAN_ON_VALIDATION_ERROR, cleanOnValidationError); + putIfSet(conf, ConfigUtils.CLEAN_DISABLED, cleanDisabled); + putIfSet(conf, ConfigUtils.OUT_OF_ORDER, outOfOrder); + putIfSet(conf, ConfigUtils.OUTPUT_QUERY_RESULTS, outputQueryResults); + putIfSet(conf, ConfigUtils.TARGET, target); + putIfSet(conf, ConfigUtils.IGNORE_MISSING_MIGRATIONS, ignoreMissingMigrations); + putIfSet(conf, ConfigUtils.IGNORE_IGNORED_MIGRATIONS, ignoreIgnoredMigrations); + putIfSet(conf, ConfigUtils.IGNORE_PENDING_MIGRATIONS, ignorePendingMigrations); + putIfSet(conf, ConfigUtils.IGNORE_FUTURE_MIGRATIONS, ignoreFutureMigrations); + putIfSet(conf, ConfigUtils.VALIDATE_MIGRATION_NAMING, validateMigrationNaming); + putIfSet(conf, ConfigUtils.PLACEHOLDER_REPLACEMENT, placeholderReplacement); + putIfSet(conf, ConfigUtils.PLACEHOLDER_PREFIX, placeholderPrefix); + putIfSet(conf, ConfigUtils.PLACEHOLDER_SUFFIX, placeholderSuffix); + putIfSet(conf, ConfigUtils.BASELINE_ON_MIGRATE, baselineOnMigrate); + putIfSet(conf, ConfigUtils.VALIDATE_ON_MIGRATE, validateOnMigrate); + putIfSet(conf, ConfigUtils.DRIVER, driver); + putIfSet(conf, ConfigUtils.CREATE_SCHEMAS, createSchemas); + + putArrayIfSet(conf, ConfigUtils.ERROR_OVERRIDES, errorOverrides); + putIfSet(conf, ConfigUtils.DRYRUN_OUTPUT, dryRunOutput); + putIfSet(conf, ConfigUtils.STREAM, stream); + putIfSet(conf, ConfigUtils.BATCH, batch); + + putIfSet(conf, ConfigUtils.ORACLE_SQLPLUS, oracleSqlplus); + putIfSet(conf, ConfigUtils.ORACLE_SQLPLUS_WARN, oracleSqlplusWarn); + + putIfSet(conf, ConfigUtils.LICENSE_KEY, licenseKey); + + if (placeholders != null) { + for (String placeholder : placeholders.keySet()) { + String value = placeholders.get(placeholder); + conf.put(ConfigUtils.PLACEHOLDERS_PROPERTY_PREFIX + placeholder, value == null ? "" : value); + } + } + + conf.putAll(ConfigUtils.propertiesToMap(mavenProject.getProperties())); + conf.putAll(loadConfigurationFromConfigFiles(workDir, envVars)); + conf.putAll(envVars); + conf.putAll(ConfigUtils.propertiesToMap(System.getProperties())); + removeMavenPluginSpecificPropertiesToAvoidWarnings(conf); + + Flyway flyway = Flyway.configure(classLoader).configuration(conf).load(); + doExecute(flyway); + } catch (Exception e) { + throw new MojoExecutionException(e.toString(), ExceptionUtils.getRootCause(e)); + } + } + + /** + * Determines the files to use for loading the configuration. + * + * @param workDir The working directory to use. + * @param envVars The environment variables converted to Flyway properties. + * @return The configuration files. + */ + private List determineConfigFiles(File workDir, Map envVars) { + List configFiles = new ArrayList<>(); + + if (envVars.containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(envVars.get(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(toFile(workDir, file)); + } + return configFiles; + } + + if (System.getProperties().containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(System.getProperties().getProperty(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(toFile(workDir, file)); + } + return configFiles; + } + + if (mavenProject.getProperties().containsKey(ConfigUtils.CONFIG_FILES)) { + for (String file : StringUtils.tokenizeToStringArray(mavenProject.getProperties().getProperty(ConfigUtils.CONFIG_FILES), ",")) { + configFiles.add(toFile(workDir, file)); + } + } else if (this.configFiles != null) { + configFiles.addAll(Arrays.asList(this.configFiles)); + } + return configFiles; + } + + /** + * Converts this fileName into a file, adjusting relative paths if necessary to make them relative to the pom. + * + * @param workDir The working directory to use. + * @param fileName The name of the file, relative or absolute. + * @return The resulting file. + */ + private File toFile(File workDir, String fileName) { + File file = new File(fileName); + if (file.isAbsolute()) { + return file; + } + return new File(workDir, fileName); + } + + /** + * Determines the encoding to use for loading the configuration files. + * + * @param envVars The environment variables converted to Flyway properties. + * @return The encoding. (default: UTF-8) + */ + private String determineConfigurationFileEncoding(Map envVars) { + if (envVars.containsKey(ConfigUtils.CONFIG_FILE_ENCODING)) { + return envVars.get(ConfigUtils.CONFIG_FILE_ENCODING); + } + if (System.getProperties().containsKey(ConfigUtils.CONFIG_FILE_ENCODING)) { + return System.getProperties().getProperty(ConfigUtils.CONFIG_FILE_ENCODING); + } + if (configFileEncoding != null) { + return configFileEncoding; + } + return "UTF-8"; + } + + /** + * Filters there properties to remove the Flyway Maven Plugin-specific ones to avoid warnings. + * + * @param conf The properties to filter. + */ + private static void removeMavenPluginSpecificPropertiesToAvoidWarnings(Map conf) { + conf.remove(ConfigUtils.CONFIG_FILES); + conf.remove(ConfigUtils.CONFIG_FILE_ENCODING); + conf.remove(CONFIG_CURRENT); + conf.remove(CONFIG_SKIP); + conf.remove(CONFIG_VERSION); + conf.remove(CONFIG_SERVER_ID); + conf.remove(CONFIG_WORKING_DIRECTORY); + } + + /** + * Retrieve the properties from the config files (if specified). + * + * @param workDir The working directory to use. + * @param envVars The environment variables converted to Flyway properties. + * @return The properties. + */ + private Map loadConfigurationFromConfigFiles(File workDir, Map envVars) { + String encoding = determineConfigurationFileEncoding(envVars); + + Map conf = new HashMap<>(); + for (File configFile : determineConfigFiles(workDir, envVars)) { + conf.putAll(ConfigUtils.loadConfigurationFile(configFile, encoding, true)); + } + return conf; + } + + /** + * Retrieve the properties from the config files (if specified). + * + * @param envVars The environment variables converted to Flyway properties. + * @return The properties. + */ + private Map loadConfigurationFromDefaultConfigFiles(Map envVars) { + String encoding = determineConfigurationFileEncoding(envVars); + File configFile = new File(System.getProperty("user.home") + "/" + ConfigUtils.CONFIG_FILE_NAME); + + return new HashMap<>(ConfigUtils.loadConfigurationFile(configFile, encoding, false)); + } + + /** + * Retrieves this property from either the system or the maven properties. + * + * @param name The name of the property to retrieve. + * @return The property value. {@code null} if not found. + */ + protected String getProperty(String name) { + String systemProperty = System.getProperty(name); + + if (systemProperty != null) { + return systemProperty; + } + + return mavenProject.getProperties().getProperty(name); + } + + /** + * Executes this mojo. + * + * @param flyway The flyway instance to operate on. + * @throws Exception any exception + */ + protected abstract void doExecute(Flyway flyway) throws Exception; +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/BaselineMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/BaselineMojo.java new file mode 100644 index 0000000..c8c75ce --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/BaselineMojo.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.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; + +/** + * Baselines an existing database, excluding all migrations up to and including baselineVersion. + */ +@Mojo(name = "baseline", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true) +public class BaselineMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) throws Exception { + flyway.baseline(); + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/CleanMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/CleanMojo.java new file mode 100644 index 0000000..c9f5f49 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/CleanMojo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2020 Redgate Software Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; + +/** + * Maven goal that drops all database objects (tables, views, procedures, triggers, ...) in the configured schemas. + * The schemas are cleaned in the order specified by the {@code schemas} property.. + */ +@SuppressWarnings({"JavaDoc", "UnusedDeclaration"}) +@Mojo(name = "clean", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.CLEAN, + threadSafe = true) +public class CleanMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) throws Exception { + flyway.clean(); + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/InfoMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/InfoMojo.java new file mode 100644 index 0000000..a0ad863 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/InfoMojo.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.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.internal.info.MigrationInfoDumper; + +/** + * Maven goal to retrieve the complete information about the migrations including applied, pending and current migrations with + * details and status. + */ +@SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) +@Mojo(name = "info", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true) +public class InfoMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) { + MigrationInfoService info = flyway.info(); + MigrationInfo current = info.current(); + MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion(); + log.info("Schema version: " + currentSchemaVersion); + log.info(""); + log.info(MigrationInfoDumper.dumpToAsciiTable(info.all())); + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLog.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLog.java new file mode 100644 index 0000000..43de87e --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLog.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.maven; + +import org.flywaydb.core.api.logging.Log; + +/** + * Wrapper around a Maven Logger. + */ +public class MavenLog implements Log { + /** + * Maven Logger. + */ + private final org.apache.maven.plugin.logging.Log logger; + + /** + * Creates a new wrapper around this logger. + * + * @param logger The original Maven Logger. + */ + MavenLog(org.apache.maven.plugin.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 b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLogCreator.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLogCreator.java new file mode 100644 index 0000000..1e8d202 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MavenLogCreator.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.maven; + +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.apache.maven.plugin.AbstractMojo; + +/** + * Log Creator for Maven Logging. + */ +public class MavenLogCreator implements LogCreator { + /** + * The Maven Mojo to log for. + */ + private final AbstractMojo mojo; + + /** + * Creates a new Maven Log Creator for this Mojo. + * + * @param mojo The Maven Mojo to log for. + */ + MavenLogCreator(AbstractMojo mojo) { + this.mojo = mojo; + } + + public Log createLogger(Class clazz) { + return new MavenLog(mojo.getLog()); + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MigrateMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MigrateMojo.java new file mode 100644 index 0000000..2dbcc67 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/MigrateMojo.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.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; + +/** + * Maven goal that triggers the migration of the configured database to the latest version. + */ +@SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) +@Mojo(name = "migrate", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true) +public class MigrateMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) throws Exception { + flyway.migrate(); + + MigrationInfo current = flyway.info().current(); + if (current != null && current.getVersion() != null) { + mavenProject.getProperties().setProperty("flyway.current", current.getVersion().toString()); + } + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/RepairMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/RepairMojo.java new file mode 100644 index 0000000..d3f9109 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/RepairMojo.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.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; + +/** + * 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
  • + *
+ */ +@SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) +@Mojo(name = "repair", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true) +public class RepairMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) throws Exception { + flyway.repair(); + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/UndoMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/UndoMojo.java new file mode 100644 index 0000000..c9a1dfa --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/UndoMojo.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.maven; + +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; + +/** + * Undoes the most recently applied versioned migration. + *

Flyway Pro and Flyway Enterprise only

+ */ +@SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) +@Mojo(name = "undo", + requiresDependencyResolution = ResolutionScope.TEST, + threadSafe = true) +public class UndoMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) { + flyway.undo(); + + MigrationInfo current = flyway.info().current(); + if (current != null && current.getVersion() != null) { + mavenProject.getProperties().setProperty("flyway.current", current.getVersion().toString()); + } + } +} \ No newline at end of file diff --git b/flyway-maven-plugin/src/main/java/org/flywaydb/maven/ValidateMojo.java a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/ValidateMojo.java new file mode 100644 index 0000000..0e96f68 --- /dev/null +++ a/flyway-maven-plugin/src/main/java/org/flywaydb/maven/ValidateMojo.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.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.flywaydb.core.Flyway; + +/** + *

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 + */ +@SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) +@Mojo(name = "validate", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true) +public class ValidateMojo extends AbstractFlywayMojo { + @Override + protected void doExecute(Flyway flyway) throws Exception { + flyway.validate(); + } +} \ No newline at end of file diff --git b/mvnw a/mvnw new file mode 100644 index 0000000..86cc337 --- /dev/null +++ a/mvnw @@ -0,0 +1,233 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven3 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} "$@" diff --git b/mvnw.cmd a/mvnw.cmd new file mode 100644 index 0000000..1d696f8 --- /dev/null +++ a/mvnw.cmd @@ -0,0 +1,142 @@ +@REM +@REM Copyright 2010-2020 Redgate Software Ltd +@REM +@REM Licensed under the Apache License, Version 2.0 (the "License"); +@REM you may not use this file except in compliance with the License. +@REM You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, software +@REM distributed under the License is distributed on an "AS IS" BASIS, +@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@REM See the License for the specific language governing permissions and +@REM limitations under the License. +@REM + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% \ No newline at end of file diff --git b/pom.xml a/pom.xml new file mode 100644 index 0000000..7f71e75 --- /dev/null +++ a/pom.xml @@ -0,0 +1,1256 @@ + + + + 4.0.0 + org.flywaydb + flyway-parent + 0-SNAPSHOT + pom + ${project.artifactId} + Flyway: Database Migrations Made Easy. + https://flywaydb.org + + + Apache License, Version 2.0 + https://flywaydb.org/licenses/flyway-community + repo + + + + + https://github.com/flyway/flyway + scm:git:${env.FLYWAY_REPO_URL} + scm:git:${env.FLYWAY_REPO_URL} + + + + + + + HEAD + + + + axel + Axel Fontaine + + + + + flyway-core + flyway-gradle-plugin + flyway-maven-plugin + flyway-commandline + + + + + + + + + + + + + ${snapshotRepository.id} + ${snapshotRepository.name} + ${snapshotRepository.url} + + + ${releaseRepository.id} + ${releaseRepository.name} + ${releaseRepository.url} + + + + + + + + + + + + + maven-central + https://repo1.maven.org/maven2 + + + + repo.gradle.org + https://repo.gradle.org/gradle/libs-releases-local/ + + + flyway-repo + https://nexus.flywaydb.org/repository/flyway/ + + + + + + + + + + + + + + + + + + + + + + + + + maven.oracle.com + + true + + + false + + https://maven.oracle.com + default + + + + + + + + + + + + + + + maven.oracle.com + https://maven.oracle.com + + + + + + 3.0 + 3.0 + 10.15.2.0 + 1.4.200 + 2.5.0 + 3.0.8 + 4.5.2 + 1.3.1 + 2.6.0 + 8.0.17 + 7.2.0.jre8 + 19.6.0.0 + 42.2.13.jre6 + 3.11.1 + 3.30.1 + 4.3.1 + 3.15.200 + 3.10.600 + 4.0.1.2 + 1.7.30 + 11.0.2 + 1.18 + 2.8.6 + + + + + + commons-logging + commons-logging + 1.2 + true + + + org.slf4j + slf4j-api + ${version.slf4j} + true + + + org.slf4j + slf4j-jdk14 + ${version.slf4j} + true + + + org.slf4j + slf4j-nop + ${version.slf4j} + true + + + org.slf4j + jcl-over-slf4j + ${version.slf4j} + true + + + org.jboss + jboss-vfs + 3.2.15.Final + true + + + org.eclipse.platform + org.eclipse.equinox.common + ${version.equinoxcommon} + true + + + org.eclipse.platform + org.eclipse.osgi + ${version.equinox} + + + org.osgi + org.osgi.core + ${version.osgi} + + + org.postgresql + postgresql + ${version.postgresql} + true + + + org.apache.derby + derby + ${version.derby} + true + + + org.apache.derby + derbytools + ${version.derby} + true + + + org.apache.derby + derbyshared + ${version.derby} + true + + + org.apache.derby + derbyclient + ${version.derby} + true + + + org.hsqldb + hsqldb + ${version.hsqldb} + true + + + com.h2database + h2 + ${version.h2} + true + + + org.firebirdsql.jdbc + jaybird-jdk18 + ${version.jaybird} + true + + + net.snowflake + snowflake-jdbc + ${version.snowflake} + true + + + org.xerial + sqlite-jdbc + ${version.sqlite} + true + + + org.mariadb.jdbc + mariadb-java-client + ${version.mariadb} + true + + + net.java.dev.jna + jna + ${version.jna} + true + + + net.java.dev.jna + jna-platform + ${version.jna} + true + + + mysql + mysql-connector-java + ${version.mysql} + true + + + com.oracle.database.jdbc + ojdbc8 + ${version.oracle} + true + + + net.sourceforge.jtds + jtds + ${version.jtds} + true + + + com.ibm.informix + jdbc + 4.50.3 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.microsoft.sqlserver + mssql-jdbc + ${version.mssql-jdbc} + true + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.google.android + android + ${version.android} + true + + + com.google.android + android-test + ${version.android} + true + + + org.apache.maven + maven-plugin-api + ${version.maven} + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${version.maven} + + + org.apache.maven + maven-artifact + ${version.maven} + + + org.apache.maven + maven-model + ${version.maven} + + + org.apache.maven + maven-core + ${version.maven} + + + org.apache.ant + ant + 1.9.14 + + + org.fusesource.jansi + jansi + ${version.jansi} + + + com.google.code.gson + gson + ${version.gson} + + + net.adoptopenjdk + jre + ${version.jre} + windows-x64 + zip + + + net.adoptopenjdk + jre + ${version.jre} + linux-x64 + tar.gz + + + net.adoptopenjdk + jre + ${version.jre} + macos-x64 + tar.gz + + + + + + + + + + + + + + + + + + + + + + + + maven-assembly-plugin + 3.2.0 + + + maven-compiler-plugin + 3.8.1 + + + maven-install-plugin + 3.0.0-M1 + + + maven-deploy-plugin + 3.0.0-M1 + + + maven-jar-plugin + 3.2.0 + + + org.apache.felix + maven-bundle-plugin + 2.5.4 + + + + + + + + + + + + + maven-dependency-plugin + 3.1.2 + + + maven-plugin-plugin + 3.6.0 + + + org.codehaus.plexus + plexus-maven-plugin + 1.3.8 + + + maven-javadoc-plugin + 3.0.1 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + org.codehaus.mojo + build-helper-maven-plugin + 1.12 + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + 3.8.2 + + + + + + maven-compiler-plugin + + + 1.8 + 1.8 + UTF-8 + + + + maven-resources-plugin + 2.7 + + UTF-8 + + nofilter + + + + + maven-source-plugin + 3.0.1 + + + attach-sources + verify + + jar + + + + + + org.codehaus.mojo + versions-maven-plugin + 2.7 + + false + + + + maven-scm-plugin + 1.11.2 + + flyway-${project.version} + + + + com.mycila + license-maven-plugin + 3.0 + false + +
${basedir}/LICENSE.txt
+ true + true + UTF-8 + + LICENSE + **/build/** + .idea/** + **/*.sh + **/*.txt + **/*.cnf + **/*.conf + **/*.releaseBackup + **/*.nofilter + **/*.ini + **/*.md + **/*.ids + **/*.ipr + **/*.iws + **/*.bin + **/*.lock + **/*.gradle + **/*.sbt + **/gradlew + .gitignore + .gitattributes + .travis.yml + **/flyway + **/*_BOM.sql + + true + + SLASHSTAR_STYLE + +
+ + + package + + format + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + maven-deploy-plugin + + true + 3 + + + + maven-javadoc-plugin + + false + UTF-8 + none + + +
+
+ + + + + maven-javadoc-plugin + 3.0.1 + + false + UTF-8 + false + + + + + + + + release-sign-artifacts + + + performRelease + true + + + + + + maven-javadoc-plugin + + + attach-sources + verify + + jar + + + + + + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + + sign + + + + --pinentry-mode=loopback + + flyway-gpg + + + + + + + + +