Skip to content

Instantly share code, notes, and snippets.

@agentgt
Created October 16, 2025 13:53
Show Gist options
  • Select an option

  • Save agentgt/d836a22e7f5875b5d5c73c5fbfd7205c to your computer and use it in GitHub Desktop.

Select an option

Save agentgt/d836a22e7f5875b5d5c73c5fbfd7205c to your computer and use it in GitHub Desktop.
Flyway prepropcessor
package com.snaphop.flyway;
import static java.lang.System.out;
import static java.util.Objects.requireNonNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.snaphop.flyway.FlywayPreprocessor.Error.ErrorCode;
import com.snaphop.flyway.FlywayPreprocessor.MigrationManifest.Version;
public class FlywayPreprocessor {
// The api is command line driven
private FlywayPreprocessor() {}
private static final String META_TAG = "----?";
@SuppressWarnings("null")
private static PrintStream out() {
return out;
}
private static class Log {
public static void info(
String message) {
out().println(message);
}
public static void error(
String message) {
out().println(message);
}
}
private enum MetaParameter {
version("v", "version"),
sources("s", "source", "sources"),
description("d", "description");
private final String[] names;
private MetaParameter(
String... ns) {
this.names = ns;
}
@Nullable
String getParameter(
Map<String, String> m) {
for (String n : names) {
String s = m.get(n);
if (s != null)
return s;
s = m.get(n.toUpperCase());
if (s != null)
return s;
}
return null;
}
}
public static class Error {
private final ErrorCode errorCode;
private final Exception exception;
public static enum ErrorCode {
MANIFEST_VERSION_MISSING_CHUNKS,
CHUNKS_VERSION_NOT_DECLARED,
CHUNK_NOT_DECLARED,
MANIFEST_VERSION_MISSING_CHUNK,
MANIFEST_VERSION_ORDER
}
private Error(
ErrorCode code,
Exception exception) {
super();
this.errorCode = code;
this.exception = exception;
}
public static Error create(
ErrorCode code,
String message) {
try {
throw new IllegalStateException(message);
}
catch (Exception e) {
return new Error(code, e);
}
}
public RuntimeException newException() {
throw new RuntimeException(this.exception);
}
@Override
public String toString() {
ErrorCode _code = errorCode;
return _code.name() + " " + exception.getMessage();
}
}
public static void main(
String[] args) {
if (args.length < 2) {
out().println("CMD workspaceDirectory outputDirectory [options]");
System.exit(64);
return;
}
Path workspaceDirectory = Paths.get(".")
.resolve(args[0]);
Path outputDirectory = Paths.get(".")
.resolve(args[1]);
boolean dryRun = false;
boolean validate = false;
if (args.length > 2) {
for (int i = 2; i < args.length; i++) {
var arg = args[i];
if (arg.equals("-n")) {
dryRun = true;
}
else if (arg.equals("-v")) {
validate = true;
dryRun = true;
}
else {
Log.error("Argument not recognized, only -n and -v allowed. arg: " + arg);
System.exit(64);
}
}
}
try {
// TODO move validation to run command.
if (validate) {
Log.info("Validating");
}
var result = run(workspaceDirectory.toFile(), outputDirectory.toFile(), dryRun);
if (!result.errors()
.isEmpty()) {
System.exit(1);
}
if (validate) {
var changes = result.updates()
.values()
.stream()
.filter(mfc -> mfc.getFlag() != MigrationFileAndContent.Flag.SKIP)
.toList();
if (!changes.isEmpty()) {
Log.error("Validation failed as there were changes! changes: " + changes);
System.exit(2);
}
}
}
catch (IOException e) {
e.printStackTrace();
System.exit(65);
}
}
record RunResult(
Map<MigrationVersion, MigrationFileAndContent> updates,
MigrationWorkspace workspace,
List<Error> errors) {}
static RunResult run(
File workspaceDirectory,
File outputDirectory,
boolean dryRun)
throws IOException {
if (dryRun) {
Log.info("Dry Run");
}
File manifestFile = new File(workspaceDirectory, "VERSIONS");
Log.info(
"Running" //
+ "\n\tworkspaceDirectory: "
+ workspaceDirectory //
+ "\n\toutputDirectory: "
+ outputDirectory //
+ "\n\tmanifestFile: "
+ manifestFile //
+ "\n" //
);
if (!workspaceDirectory.isDirectory()) {
throw new IOException(
"Workspace directory is not a directory or does not exist. workspaceDir: " + workspaceDirectory);
}
if (!outputDirectory.isDirectory()) {
throw new IOException(
"Output directory is not a directory or does not exist. outputDir: " + outputDirectory);
}
List<Error> errors = new ArrayList<>();
MigrationWorkspace workspace = MigrationWorkspace.create(manifestFile, workspaceDirectory);
Map<MigrationVersion, Map<Path, Chunk>> chunks = workspace.chunks();
SortedMap<MigrationVersion, Version> manifestVersions = workspace.manifest()
.versions();
Map<MigrationVersion, MigrationFile> migrations = MigrationFile.readFromDirectory(outputDirectory);
Map<MigrationVersion, MigrationFileAndContent> updates = new TreeMap<>();
Version prev = null;
for (Entry<MigrationVersion, Version> e : manifestVersions.entrySet()) {
Version v = e.getValue();
MigrationVersion mv = e.getKey();
if (prev == null) {
prev = v;
}
else if (prev.lineNumber() >= v.lineNumber()) {
Error er = Error.create(
ErrorCode.MANIFEST_VERSION_ORDER,
"Manifest declared version out of order. version: " + v + "\nprevious version:" + prev);
errors.add(er);
}
if (!chunks.containsKey(mv)) {
Error er = Error.create(
ErrorCode.MANIFEST_VERSION_MISSING_CHUNKS,
"Manifest declared version but no chunks found for version. version: " + v);
errors.add(er);
MigrationFile _file = MigrationFile.create(outputDirectory, v, "");
Error _error = er;
MigrationFileAndContent.Flag _flag = MigrationFileAndContent.Flag.MISSING;
MigrationFileAndContent mfc = new MigrationFileAndContent(_file, _error, _flag);
updates.put(mv, mfc);
}
}
for (Entry<MigrationVersion, Map<Path, Chunk>> e : chunks.entrySet()) {
MigrationVersion v = e.getKey();
Map<Path, Chunk> cs = e.getValue();
// boolean manifestContains = manifestVersions.containsKey(v);
Version version = manifestVersions.get(v);
if (version == null) {
StringBuilder hint =
new StringBuilder("\n\nHint add something like (the source order maybe incorrect!) to '");
hint.append(manifestFile)
.append("':\n\n");
hint.append(v.getVersion())
.append("?s=\\\n");
hint.append(
cs.keySet()
.stream()
.map(p -> "\t," + p.toString())
.collect(Collectors.joining("\\\n")));
hint.append("\n");
errors.add(
Error.create(
ErrorCode.CHUNKS_VERSION_NOT_DECLARED,
"Chunks declared for version but version not in manifest. version: "
+ v
+ " chunks:\n"
+ cs
+ hint));
continue;
}
Collection<Path> paths = version.sourcesAsFiles(workspaceDirectory);
for (Entry<Path, Chunk> ep : cs.entrySet()) {
if (!paths.contains(ep.getKey())) {
errors.add(
Error.create(
ErrorCode.CHUNK_NOT_DECLARED,
"Chunks declared for version but chunk not in manifest. verions: "
+ v
+ " chunk: "
+ ep.getValue()));
continue;
}
}
if (paths.isEmpty()) {
errors.add(
Error.create(
ErrorCode.MANIFEST_VERSION_MISSING_CHUNKS,
"Manifest Version declares no sources. version: " + version));
continue;
}
StringBuilder sb = new StringBuilder();
sb.append("-- VERSION: ")
.append(v)
.append("\n");
for (Path p : paths) {
Chunk c = cs.get(p);
if (c == null) {
errors.add(
Error.create(
ErrorCode.MANIFEST_VERSION_MISSING_CHUNK,
"Path declared for version but no chunks found."
+ " path: "
+ p
+ " version: "
+ version));
continue;
}
sb.append("\n-- START FROM: ")
.append(
c.file()
.getName())
.append("\n");
sb.append(
c.rawSQL()
.trim());
sb.append("\n-- END FROM: ")
.append(
c.file()
.getName())
.append("\n");
}
String content = sb.toString();;
MigrationFile file = MigrationFile.create(outputDirectory, version, content);
MigrationFileAndContent.Flag flag = MigrationFileAndContent.Flag.CREATE;
MigrationFileAndContent mfc = new MigrationFileAndContent(file, flag, content);
updates.put(v, mfc);
}
for (Entry<MigrationVersion, MigrationFile> e : migrations.entrySet()) {
MigrationVersion v = e.getKey();
MigrationFile f = e.getValue();
MigrationFileAndContent mfc = updates.get(v);
if (mfc != null) {
mfc.originalFile = f;
if (mfc.flag == MigrationFileAndContent.Flag.MISSING) {
// noop
}
else if (!mfc.file.description()
.equals(f.description())) {
mfc.flag = MigrationFileAndContent.Flag.RENAME;
}
else if (mfc.file.checksum()
.equals(f.checksum())) {
mfc.flag = MigrationFileAndContent.Flag.SKIP;
mfc.file = f;
}
else {
mfc.flag = MigrationFileAndContent.Flag.UPDATE;
mfc.file = f;
}
}
else {
Log.info("Manifest has no entry for version: " + v);
}
}
for (Error e : errors) {
Log.error("Error: " + e);
}
if (!errors.isEmpty()) {
Log.info("Because of errors setting dryRun = true");
dryRun = true;
}
for (Entry<MigrationVersion, MigrationFileAndContent> e : updates.entrySet()) {
// MigrationVersion v = e.getKey();
MigrationFileAndContent mfc = e.getValue();
process(mfc, workspaceDirectory, outputDirectory, dryRun);
}
return new RunResult(updates, workspace, errors);
}
private static void process(
MigrationFileAndContent mfc,
File workspaceDirectory,
File outputDirectory,
boolean dryRun)
throws IOException {
switch (mfc.flag) {
case CREATE:
Log.info("Creating " + mfc.file.file());
if (!dryRun) {
mfc.writeFile();
}
break;
case SKIP:
Log.info("Skipping " + mfc.file.file());
break;
case UPDATE:
Log.info("Updating " + mfc.file.file());
if (!dryRun) {
// mfc.backupOriginal();
mfc.writeFile();
}
break;
case RENAME:
Log.info("Renaming \n\t" + mfc.originalFile.file() + "\n\tto\n\t" + mfc.file.file());
if (!dryRun) {
// mfc.backupOriginal();
mfc.writeFile();
}
break;
case MISSING:
Log.info(
"Missing chunks for "
+ mfc.file.file()
.getName());
break;
default:
break;
}
}
record MigrationWorkspace(
File workspaceDirectory,
MigrationManifest manifest,
Map<MigrationVersion, Map<Path, Chunk>> chunks) {
static MigrationWorkspace create(
final File manifestFile,
final File workspaceDirectory)
throws IOException {
Log.info("Reading manifest: " + manifestFile);
MigrationManifest manifest = MigrationManifest.parseFile(manifestFile.getCanonicalPath());
Log.info("Reading workspace directory: " + workspaceDirectory);
Map<MigrationVersion, Map<Path, Chunk>> chunks = parseFromDirectory(workspaceDirectory);
return new MigrationWorkspace(workspaceDirectory, manifest, chunks);
}
public static Map<MigrationVersion, Map<Path, Chunk>> parseFromDirectory(
File directory)
throws IOException {
Set<String> resources = findResourceNamesFromFileSystem(directory);
TreeMap<MigrationVersion, Map<Path, Chunk>> m = new TreeMap<>();
for (String r : resources) {
if (r.endsWith(".sql")) {
List<Chunk> chunks = Chunk.parseFile(r);
for (Chunk c : chunks) {
MigrationVersion version = MigrationVersion.fromVersion(c.getVersion());
Map<Path, Chunk> tmp = m.get(version);
if (tmp == null) {
tmp = new TreeMap<>();
m.put(version, tmp);
}
Path relativePath = directory.toPath()
.relativize(
c.file()
.toPath());
if (tmp.containsKey(relativePath)) {
throw new IllegalStateException(
"Multiple chunks registered for same version. bad chunk: " + c);
}
tmp.put(relativePath, c);
}
}
}
return m;
}
}
static String md5(
String contents) {
try {
return printHexBinary(
MessageDigest.getInstance("MD5")
.digest(contents.getBytes(StandardCharsets.UTF_8)));
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
private static String printHexBinary(
byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
// Mutable class!
static class MigrationFileAndContent {
private MigrationFile file;
private MigrationFile originalFile;
private @Nullable String content;
private Flag flag;
@SuppressWarnings("unused") // for later enhancements
private @Nullable Error error;
public static enum Flag {
CREATE,
UPDATE,
RENAME,
SKIP,
MISSING;
}
public MigrationFileAndContent(
MigrationFile file,
Flag flag,
String content) {
this.file = file;
this.flag = flag;
this.content = content;
this.originalFile = file;
}
public MigrationFileAndContent(
MigrationFile file,
Error error,
Flag flag) {
this.file = file;
this.error = error;
this.flag = flag;
this.originalFile = file;
}
public void writeFile()
throws IOException {
String _content = content;
if (_content == null || _content.isEmpty()) {
throw new IOException("This file is not to be written as it has errors or is empty");
}
Files.write(
file.file()
.toPath(),
_content.getBytes(StandardCharsets.UTF_8));
}
public void backupOriginal()
throws IOException {
MigrationFile of = requireNonNull(originalFile);
Path p = of.file()
.toPath();
String name = p.toFile()
.getName();
String nn = name + ".orig";
Path np = p.getParent()
.resolve(nn);
Files.move(
of.file()
.toPath(),
np);
}
Flag getFlag() {
return flag;
}
@Nullable
Error getError() {
return error;
}
@Override
public String toString() {
return "MigrationFileAndContent [file="
+ file.file()
+ ", originalFile="
+ originalFile.file()
+ ", flag="
+ flag
+ ", error="
+ error
+ "]";
}
}
record MigrationFile(
File file,
String rawName,
String rawVersion,
MigrationVersion version,
String description,
String checksum,
Source source) {
enum Source {
FILE,
SOURCE
}
static MigrationFile create(
File outputDirectory,
Version version,
String content) {
File file = new File(outputDirectory, version.toFileName());
String rawVersion = version.version()
.toString();
MigrationVersion v = version.version();
String description = version.description();
String checksum = md5(content);
return new MigrationFile(file, content, rawVersion, v, description, checksum, Source.SOURCE);
}
static String stripSqlExt(
String filename) {
if (filename.endsWith(".sql")) {
return filename.substring(0, filename.lastIndexOf(".sql"));
}
return filename;
}
static MigrationFile create(
final File f)
throws IOException {
if (!isValidFile(f)) {
throw new IllegalArgumentException("bad migration file: " + f);
}
String md5 = md5(readFile(f.getCanonicalPath()));
return create(f, md5);
}
static MigrationFile create(
final File f,
String checksum)
throws IOException {
if (!isValidFile(f)) {
throw new IllegalArgumentException("bad migration file: " + f);
}
final String rawName = f.getName();
@NonNull
String[] pair = rawName.split("__", 2);
String p = pair[0];
String description = pair.length == 2 ? pair[1] : "";
description = stripSqlExt(description);
if (description.equals("")) {
p = stripSqlExt(p);
}
String rawVersion = p.substring(1);
MigrationVersion version = MigrationVersion.fromVersion(rawVersion);
return new MigrationFile(f, rawName, rawVersion, version, description, checksum, Source.FILE);
}
static String readFile(
String path)
throws IOException {
byte[] encoded = Files.readAllBytes(Paths.get(path));
return new String(encoded, StandardCharsets.UTF_8);
}
static Map<MigrationVersion, MigrationFile> readFromDirectory(
File outputDirectory)
throws IOException {
Set<String> paths = findResourceNamesFromFileSystem(outputDirectory);
TreeMap<MigrationVersion, MigrationFile> files = new TreeMap<>();
for (String p : paths) {
File f = new File(p);
if (MigrationFile.isValidFile(f)) {
MigrationFile mf = MigrationFile.create(f);
files.put(mf.version(), mf);
}
}
return files;
}
static boolean isValidFile(
File f) {
final String rawName = f.getName();
if (f.isDirectory()) {
return false;
}
if (!rawName.startsWith("V"))
return false;
if (!rawName.endsWith(".sql")) {
return false;
}
return true;
}
@Override
public String toString() {
return "MigrationFile [file="
+ file
+ ", rawName="
+ rawName
+ ", rawVersion="
+ rawVersion
+ ", version="
+ version
+ ", description="
+ description
+ ", checksum="
+ checksum
+ "]";
}
}
private static Set<String> findResourceNamesFromFileSystem(
File folder) {
Set<String> 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(file));
}
}
else {
resourceNames.add(file.getPath());
}
}
}
return resourceNames;
}
record MigrationManifest(
String file,
SortedMap<MigrationVersion, Version> versions) {
record Version(
String raw,
int lineNumber,
String rawVersion,
MigrationVersion version,
String rawParameters,
Map<String, String> parameters,
List<String> sources,
String description) {
String toFileName() {
final String prefix = "V" + version.toString();
if (requireNonNull(description).isEmpty()) {
return prefix + ".sql";
}
return prefix + "__" + description + ".sql";
}
static List<String> commaSplit(
@Nullable String source) {
if (source == null) {
return Collections.<String> emptyList();
}
String[] ar = source.split(",");
List<String> list = new ArrayList<>(ar.length);
for (String a : ar) {
if (a == null)
continue;
String s = a.trim();
if (s.isEmpty())
continue;
list.add(s);
}
return list;
}
static Version parseVersion(
final String raw,
final int lineNumber) {
URI uri = URI.create(raw);
final String rawVersion = uri.getPath();
requireNonNull(rawVersion, () -> "version missing at line: " + lineNumber);
final MigrationVersion version = MigrationVersion.fromVersion(rawVersion);
final String rawParameters = uri.getRawQuery();
requireNonNull(rawParameters, () -> "parameters missing at line: " + lineNumber);
final Map<String, String> parameters = parseUriLikeQuery(rawParameters, true);
String s = MetaParameter.sources.getParameter(parameters);
final List<String> sources = commaSplit(s);
String description = MetaParameter.description.getParameter(parameters);
description = description == null ? "" : description;
return new Version(
raw,
lineNumber,
rawVersion,
version,
rawParameters,
parameters,
sources,
description);
}
Collection<Path> sourcesAsFiles(
File workspaceDirectory) {
Path ws = workspaceDirectory.toPath();
LinkedHashSet<Path> files = new LinkedHashSet<>();
for (String s : sources()) {
final Path p;
if (!s.endsWith(".sql")) {
p = new File(workspaceDirectory, s + ".sql").toPath();
}
else {
p = new File(workspaceDirectory, s).toPath();
}
files.add(ws.relativize(p));
}
return files;
}
@Override
public String toString() {
return toString("\n", ",\n\t", "\n");
}
public String toString(
String start,
String sep,
String end) {
return start
+ "Version [raw="
+ raw
+ sep
+ "lineNumber="
+ lineNumber
+ sep
+ "version="
+ version
+ sep
+ "description="
+ description
+ sep
+ "sources="
+ sources
+ "]"
+ end;
}
}
static MigrationManifest parseFile(
String file)
throws IOException {
File f = new File(file);
try (InputStreamReader fr = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)) {
return parseFile(file, fr);
}
}
static MigrationManifest parseFile(
final String file,
Reader reader)
throws IOException {
BufferedReader br = new BufferedReader(reader);
String line;
List<Version> rawVersions = new ArrayList<>();
int i = 0;
int lineStart = 0;
StringBuilder current = new StringBuilder();
boolean wrap = false;
while ((line = br.readLine()) != null) {
line = line.trim();
if (!wrap) {
lineStart = i;
}
if (line.endsWith("\\")) {
wrap = true;
current.append(line.substring(0, line.length() - 1));
}
else {
wrap = false;
current.append(line);
String code = current.toString()
.trim();
if (!code.isEmpty()) {
Version v;
try {
v = Version.parseVersion(code, lineStart);
}
catch (RuntimeException e) {
String error = String.format(
"Version parse error." + "\n\tfile=%s, line=%s, entry=%s, reason=\n\t\t%s",
file,
(lineStart + 1),
code,
e.getMessage());
throw new IllegalStateException(error, e);
}
rawVersions.add(v);
}
current.setLength(0);
}
i++;
}
TreeMap<MigrationVersion, Version> versions = new TreeMap<>();
for (Version v : rawVersions) {
versions.put(v.version(), v);
}
MigrationManifest vm = new MigrationManifest(file, Collections.unmodifiableSortedMap(versions));
return vm;
}
@Override
public String toString() {
return "VersionsManifest [file=" + file + ", versions=" + versions + "]";
}
}
record Chunk(
File file,
int lineNumberStart,
int lineNumberEnd,
String rawMeta,
String rawSQL,
Map<String, String> meta) {
static Chunk of(
File file,
int lineNumberStart,
int lineNumberEnd,
String rawMeta,
String rawSQL) {
try {
Map<String, String> meta = Collections.unmodifiableMap(parseMeta(rawMeta));
return new Chunk(file, lineNumberStart, lineNumberEnd, rawMeta, rawSQL, meta);
}
catch (Exception e) {
String error = String.format(
"Chunk parse error." + "\n\tfile=%s, lineStart=%s, lineEnd=%s",
file,
lineNumberStart,
lineNumberEnd);
throw new IllegalStateException(error, e);
}
}
@Override
public String toString() {
return toString("\n", ",\n\t", "\n");
}
String toString(
String start,
String sep,
String end) {
return start
+ "Chunk [file="
+ file
+ sep
+ "lineNumberStart="
+ lineNumberStart
+ sep
+ "lineNumberEnd="
+ lineNumberEnd
+ "]"
+ end;
}
public @Nullable String getVersion() {
return MetaParameter.version.getParameter(meta);
}
public Map<String, String> parseMeta() {
return parseMeta(rawMeta);
}
static Map<String, String> parseMeta(
String rawMeta) {
String query = rawMeta.substring(META_TAG.length())
.trim();
return parseUriLikeQuery(query, true);
}
// ----?V=2019_01_02
static List<Chunk> parseFile(
final String file,
Reader reader)
throws IOException {
BufferedReader br = new BufferedReader(reader);
String line = null;
List<Chunk> chunks = new ArrayList<FlywayPreprocessor.Chunk>();
int lineNumberStart = 0;
int lineNumberEnd = 0;
String rawMeta = null;
StringBuilder rawSQL = new StringBuilder();
boolean started = false;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.startsWith(META_TAG)) {
if (started) {
lineNumberEnd = i;
chunks.add(
Chunk.of(
new File(file),
lineNumberStart,
lineNumberEnd,
requireNonNull(rawMeta, "bug"),
rawSQL.toString()));
rawSQL.setLength(0);
rawMeta = line;
lineNumberStart = i;
}
else {
started = true;
rawMeta = line;
lineNumberStart = i;
}
}
else {
if (started) {
rawSQL.append(line)
.append("\n");
}
}
i++;
}
if (started) {
lineNumberEnd = i;
chunks.add(
Chunk.of(
new File(file),
lineNumberStart,
lineNumberEnd,
requireNonNull(rawMeta, "bug"),
rawSQL.toString()));
}
return chunks;
}
static List<Chunk> parseFile(
final String file)
throws IOException {
File f = new File(file);
try (InputStreamReader fr = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)) {
return parseFile(file, fr);
}
}
}
static Map<String, String> parseUriLikeQuery(
@Nullable String query,
boolean decode) {
Map<String, String> queryPairs = new LinkedHashMap<String, String>();
if (query == null)
return queryPairs;
String[] pairs = query.split("[& ]");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (decode) {
queryPairs.put(
URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8),
URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8));
}
else {
queryPairs.put(pair.substring(0, idx), pair.substring(idx + 1));
}
}
return queryPairs;
}
}
/*
* Copyright 2010-2019 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.
*/
package com.snaphop.flyway;
import static java.util.Objects.requireNonNull;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.Nullable;
/**
* A version of a migration.
*
* @author Axel Fontaine
*/
public final class MigrationVersion implements Comparable<MigrationVersion> {
/**
* 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 >>");
/**
* Compiled pattern for matching proper version format
*/
private static Pattern splitPattern = 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<BigInteger> versionParts;
/**
* The printable text to represent the version.
*/
private final String displayText;
/**
* Factory for creating 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("null")
public static MigrationVersion fromVersion(
@Nullable String version) {
if ("current".equalsIgnoreCase(version))
return CURRENT;
if (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. <br/>
* {@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. <br/>
* {@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(
@Nullable BigInteger version,
String displayText) {
this.versionParts = new ArrayList<>();
if (version != null) {
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 @Nullable String getVersion() {
if (this.equals(EMPTY))
return null;
if (this.equals(LATEST))
return Long.toString(Long.MAX_VALUE);
return displayText;
}
@Override
public boolean equals(
@Nullable 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.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();
}
public int compareTo(
MigrationVersion o) {
requireNonNull(o);
// if (o == null) {
// return 1;
// }
if (this == EMPTY) {
return o == EMPTY ? 0 : Integer.MIN_VALUE;
}
if (this == CURRENT) {
return o == CURRENT ? 0 : Integer.MIN_VALUE;
}
if (this == LATEST) {
return o == LATEST ? 0 : Integer.MAX_VALUE;
}
if (o == EMPTY) {
return Integer.MAX_VALUE;
}
if (o == CURRENT) {
return Integer.MAX_VALUE;
}
if (o == LATEST) {
return Integer.MIN_VALUE;
}
final List<BigInteger> parts1 = versionParts;
final List<BigInteger> 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<BigInteger> elements,
int i) {
return i < elements.size() ? elements.get(i) : BigInteger.ZERO;
}
/**
* Splits this string into list of Long
*
* @param str
* The string to split.
* @return The resulting array.
*/
private List<BigInteger> tokenize(
String str) {
List<BigInteger> parts = new ArrayList<>();
try {
for (String part : splitPattern.split(str)) {
parts.add(new BigInteger(part));
}
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Invalid version containing non-numeric characters. Only 0..9 and . are allowed. Invalid version: "
+ str);
}
for (int i = parts.size() - 1; i > 0; i--) {
if (!parts.get(i)
.equals(BigInteger.ZERO)) {
break;
}
parts.remove(i);
}
return parts;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment