Reputation: 510
In the project we use Flyway migration tool to develop db schema along with git history of those scripts.
The idea is that each script number is prefixed with number increasing by the value of 10 and after that the name, so for example:
etc.
However since we had to start working on multiple branches (two main branches are for example: master + master_ext) plus we have some dev-branches for time-consuming features - there is a problem with choosing right script's numbers.
When having long-term dev-branch introducing few changescripts, when merging it to either master or master_ext, where someone had also introduced same numbers of different scripts we would have conflicts. (merge will probably succeed due to differences in names but flyway migration will fail)
Is there any pattern or good practice how to maintain SQL scripts versions when working on multiple branches?
Thanks in advance
Upvotes: 2
Views: 2292
Reputation: 4013
You should not that the version of a migration is the part up to th "__
" (double underscore) where the description part is starting.
The naming scheme for migrations (according to documentation on migrations) is:
<flag char><version><separator><description><suffix>
(actually much of this is configurable, but just assume it has not been changed)
__
" (double underscore).sql
)As version may be anything you could easily introduce a component that is specific to a developer or branch and by such means will help ensuring unique names.
(BTW: according to the documentation, the names you showed in the question are not proper flyway migration names!)
HOWEVER:
You are still left with the problem of ensuring a valid ordering according to logical dependencies.
Luckily, in most cases ordering constraints only occur within a unit of development, usually within a single branch. So, ensuring that all related migrations are executed in order will give a proper result. To get a useful ordering across different units of development, you might just revert to time scale.
E.g. using something along: <yyymmdd>.<unitseq>.<subseq>
where
For more complex cases that require proper ordering of development strains you might still need to watch out for proper ordering across units of development.
Back to your example from the question:
As you do not have a separator with your names, the following variants still would be assumed to be different by flyway:
Following my suggestion you might turn to using:
Be aware, that the execution order in both versions is different!
Upvotes: 1
Reputation: 916
Flyway does not provide any in build features to perform the same.
However I have done this for my project and i think so far this is the best we can do.
Below is a sample I use currently
package com.demo;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
public class GenerateFileVersion {
public static void main (String... args){
GenerateFileVersion f=new GenerateFileVersion();
f.fileVersion();
}
private String trimOrPad(String str, int length, char padChar) {
String result;
if (str == null) {
result = "";
} else {
result = str;
}
if (result.length() > length) {
return result.substring(0, length);
} else {
while (result.length() < length) {
result = padChar+result;
}
return result;
}
}
private String fileVersion(){
Date date = new Date();
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CST6CDT"));
calendar.setTime(date);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
StringBuffer sb=new StringBuffer();
sb.append(calendar.get(Calendar.YEAR)%100);
sb.append(".");
sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.MONTH)+1),2,'0'));
sb.append(".");
sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.DATE)),2,'0'));
sb.append(".");
sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.HOUR_OF_DAY)),2,'0'));
sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.MINUTE)),2,'0'));
sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.SECOND)),2,'0'));
System.out.println("Choose Your Next File Name From below list...");
int i=0;
for(ENVIRONMENT env: ENVIRONMENT.values()){
System.out.println("Next File Name for Making DDL Change : "+"V"+sb.toString()+this.trimOrPad(String.valueOf(i++),2,'0')+"__"+env.toString()+"_DDL.sql");
System.out.println("Next File Name for Making DML Change : "+"V"+sb.toString()+this.trimOrPad(String.valueOf(i++),2,'0')+"__"+env.toString()+"_DML.sql");
}
return sb.toString();
}
private enum ENVIRONMENT{COMMON(1),LOCAL(9),STAGE(4),MTF(5),PERF(7),PROD(2);
private int value;
private ENVIRONMENT(int value) { this.value = value; }
}
}
You can also add more features to your project to make sure all developers do not do any mistake you can add below program as junit test case so that build fails if there are any files does not follow flyway versioning standard.
Sample is below
package com.demo;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootRestApplicationTests {
@Test
public void checkDuplicateVersion() {
System.out.println("Starting checkDuplicateVersion");
List<ClassLoader> classLoadersList = new LinkedList<>();
classLoadersList.add(ClasspathHelper.contextClassLoader());
classLoadersList.add(ClasspathHelper.staticClassLoader());
Reflections reflections = new Reflections(new ConfigurationBuilder()
.setScanners(new ResourcesScanner())
.setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("local"))));
Reflections reflections1 = new Reflections(new ConfigurationBuilder()
.setScanners(new ResourcesScanner())
.setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("common"))));
Stream<String> stream1= reflections.getStore().get("ResourcesScanner").keys().stream();
Stream<String> stream2= reflections1.getStore().get("ResourcesScanner").keys().stream();
Stream<String> resultingStream=Stream.of(stream1,stream2).flatMap(i -> i);
//resultingStream.forEach(System.out::println);
List<String> existingFileVersions=
resultingStream
.filter(f -> f.startsWith("V"))
.filter(f -> f.endsWith(".sql"))
//.forEach(System.out::println);
.map(n -> n.split("__")[0].substring(1))
//.forEach(System.out::println);
.collect(Collectors.toList());
Set<String> duplicateVersion=existingFileVersions.stream().filter(i -> Collections.frequency(existingFileVersions, i) >1)
.collect(Collectors.toSet());
duplicateVersion.forEach( i -> System.out.println("Duplicate Version found "+i));
Assert.assertEquals(0,duplicateVersion.size());
}
@Test
public void checkFlywayFileNamingStandard(){
System.out.println("Starting checkFlywayFileNamingStandard");
List<ClassLoader> classLoadersList = new LinkedList<>();
classLoadersList.add(ClasspathHelper.contextClassLoader());
classLoadersList.add(ClasspathHelper.staticClassLoader());
Reflections reflections = new Reflections(new ConfigurationBuilder()
.setScanners(new ResourcesScanner())
.setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("local"))));
Reflections reflections1 = new Reflections(new ConfigurationBuilder()
.setScanners(new ResourcesScanner())
.setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("common"))));
Stream<String> stream1= reflections.getStore().get("ResourcesScanner").keys().stream();
Stream<String> stream2= reflections1.getStore().get("ResourcesScanner").keys().stream();
Stream<String> resultingStream=Stream.of(stream1,stream2).flatMap(i -> i);
//resultingStream.forEach(System.out::println);
resultingStream
.filter(f -> f.endsWith(".sql"))
.forEach(n -> {
if(!n.split("__")[0].toUpperCase().startsWith("V")){
System.out.println("File starts with " + n + " Does not start with Letter V or v. Please fix it.");
Assert.fail();
}
for(String s : n.split("__")[0].substring(1).split("\\.")){
try {
//System.out.println(n);
Integer.valueOf(s);
}catch(Exception e){
//e.printStackTrace();
System.out.println("File starting with "+ n + " does not match flyway standard");
System.out.println("Flyway standard is V{version}__{description}.sql");
Assert.fail();
}
}
});
}
}
For more details you can see my project here
Upvotes: 3