diff --git a/README.md b/README.md index 695777c..e2858cd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ ## Asm - equals function for registered primitive conversion types - IConst0Fixer: IConst0/1 on its own => false/true +- IConst0Fixer: IConst0/1 false/true depending on compared field type - actually parse getter - resolve Predicate functions (not, and, or) - don't bind external vars in lambdas, instead make information (parameter and intercepted values) available in a context object diff --git a/pom.xml b/pom.xml index 30633b9..340230a 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,29 @@ ${project.build.sourceEncoding} + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + jef.main.Main + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + + ${project.basedir}/target/test-generated-migrations/target + + + @@ -146,7 +169,15 @@ ${mockito.version} test - + + + mysql + mysql-connector-java + 8.0.30 + test + + + diff --git a/src/main/java/jef/Database.java b/src/main/java/jef/Database.java new file mode 100644 index 0000000..b2945a2 --- /dev/null +++ b/src/main/java/jef/Database.java @@ -0,0 +1,26 @@ +package jef; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +@Getter +@AllArgsConstructor +public abstract class Database { + // private final DatabaseConfiguration config; + protected final Connection connection; + protected final DatabaseOptions options; + + public abstract void migrate() throws MigrationException; + +// public ResultSet executeRaw(String s) throws SQLException { +// try (var stmt = connection.prepareStatement(s)) { +// try (var res = stmt.executeQuery()) { +// return res; +// } +// } +// } +} diff --git a/src/main/java/jef/DatabaseOptions.java b/src/main/java/jef/DatabaseOptions.java new file mode 100644 index 0000000..868afc8 --- /dev/null +++ b/src/main/java/jef/DatabaseOptions.java @@ -0,0 +1,46 @@ +package jef; + +import lombok.Getter; + +import java.net.MalformedURLException; +import java.util.regex.Pattern; + +@Getter +public class DatabaseOptions { + protected final String url; + protected final String user; + protected final String password; + + protected final String migrationsPackage; + + protected final String host; + protected final String database; + + public DatabaseOptions(String url, String user, String password, String migrationsPackage) { + this.url = url; + this.user = user; + this.password = password; + + host = extractHost(url); + database = extractDatabase(url); + this.migrationsPackage = migrationsPackage; + } + + private static String extractHost(String url) { + var pattern = Pattern.compile("^.*?//(.*?)/.*$"); + var matcher = pattern.matcher(url); + if (!matcher.matches()) { + throw new RuntimeException(new MalformedURLException("Could not extract host for url: " + url)); + } + return matcher.group(1); + } + + private static String extractDatabase(String url) { + var pattern = Pattern.compile("^.*?//.*?/(.*?)([?/].*)?$"); + var matcher = pattern.matcher(url); + if (!matcher.matches()) { + throw new RuntimeException(new MalformedURLException("Could not extract database for url: " + url)); + } + return matcher.group(1); + } +} diff --git a/src/main/java/jef/MigrationException.java b/src/main/java/jef/MigrationException.java new file mode 100644 index 0000000..0f83e89 --- /dev/null +++ b/src/main/java/jef/MigrationException.java @@ -0,0 +1,18 @@ +package jef; + +public class MigrationException extends Exception { + public MigrationException() { + } + + public MigrationException(String message) { + super(message); + } + + public MigrationException(String message, Throwable cause) { + super(message, cause); + } + + public MigrationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/jef/main/Main.java b/src/main/java/jef/main/Main.java new file mode 100644 index 0000000..35063de --- /dev/null +++ b/src/main/java/jef/main/Main.java @@ -0,0 +1,27 @@ +package jef.main; + +import java.util.Arrays; + +public class Main { + public static void main(String[] args) { + if (args.length == 0) { + printHelp(); + System.exit(1); + } + switch (args[0].toLowerCase()) { + case "help": + printHelp(); + case "migration": + MigrationCommandHandler.handleMigration(Arrays.copyOfRange(args, 1, args.length)); + + default: + printHelp(); + } + } + + static void printHelp() { + System.out.println("Usage: java -jar thisfile [options]"); + MigrationCommandHandler.printHelp(); + System.exit(1); + } +} diff --git a/src/main/java/jef/main/MigrationCommandHandler.java b/src/main/java/jef/main/MigrationCommandHandler.java new file mode 100644 index 0000000..31402de --- /dev/null +++ b/src/main/java/jef/main/MigrationCommandHandler.java @@ -0,0 +1,294 @@ +package jef.main; + +import jef.Database; +import jef.model.DbContext; +import jef.model.DbContextOptions; +import jef.model.ModelBuilder; +import jef.model.migration.creator.MigrationCreator; +import jef.mysql.MysqlDatabase; +import jef.util.Util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class MigrationCommandHandler { + static void handleMigration(String[] args) { + if (args.length == 0) { + Main.printHelp(); + } + + try { + switch (args[0].toLowerCase()) { + case "add": + handleMigrationAdd(Arrays.copyOfRange(args, 1, args.length)); + case "remove": + handleMigrationRemove(Arrays.copyOfRange(args, 1, args.length)); + case "list": + handleMigrationList(Arrays.copyOfRange(args, 1, args.length)); + default: + Main.printHelp(); + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + + System.exit(0); + } + + private static void handleMigrationList(String[] args) { + } + + private static void handleMigrationRemove(String[] args) { + } + + private static void handleMigrationAdd(String[] args) throws Exception { + String contextName = null; + String migrationPackage = null; + String targetFolder = null; + String name = null; + var classPath = new ArrayList(); + + for (int i = 0; i < args.length; i++) { + switch (args[i].toLowerCase()) { + case "--cp": + if (i + 1 >= args.length) { + Main.printHelp(); + } + classPath.addAll(List.of(args[++i].split(":")));//TODO does not work well on windows (:) + break; + case "--context": + case "-c": + if (i + 1 >= args.length) { + Main.printHelp(); + } + contextName = args[++i]; + break; + case "--output": + case "-o": + if (i + 1 >= args.length) { + Main.printHelp(); + } + targetFolder = args[++i]; + break; + default: + if (name != null) { + Main.printHelp(); + } + name = args[i]; + break; + } + } + +// if (Set.of(contextName, targetFolder, name).contains(null)) { +// Main.printUsage(); +// } + + if (name == null) { + System.err.println("The migration requires a name."); + System.exit(1); + } + + //find data locations + var urls = classPath.stream().map(e -> Util.tryGet(() -> new File(e).toURI().toURL()).orElseThrow()).toArray(URL[]::new); + var cl = new URLClassLoader(urls, MigrationCommandHandler.class.getClassLoader()); + var context = contextName == null ? findAnyContext(cl) : findContextByName(cl, contextName); + targetFolder = targetFolder == null ? System.getProperty("user.dir") : targetFolder; + var targetFolderFile = new File(targetFolder); + targetFolderFile.mkdirs(); + migrationPackage = findMigrationPackageName(targetFolder); + + //find data + var currentSnapshotFile = new File(targetFolderFile, "CurrentSnapshot.java"); + var from = !currentSnapshotFile.isFile() + ? new ModelBuilder() + : (ModelBuilder) cl.loadClass((migrationPackage != null ? migrationPackage + "." : "") + "CurrentSnapshot").getConstructor().newInstance(); + var to = ModelBuilder.from(context.orElseThrow().getClass()); +// context.orElseThrow().onModelCreate(to); + + //begin + var date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + var className = "M" + date + "_" + name; + + var creator = new MigrationCreator(); + var result = creator.createMigration(from, to, className, migrationPackage, currentSnapshotFile.isFile() ? Files.readString(currentSnapshotFile.toPath()) : null); + + Files.writeString(new File(targetFolder, className + ".java").toPath(), result.getMigration(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + Files.writeString(new File(targetFolder, className + "Snapshot.java").toPath(), result.getMigrationSnapshot(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + Files.writeString(new File(targetFolder, "CurrentSnapshot.java").toPath(), result.getCurrentSnapshot(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + System.exit(0); + } + + private static Optional findAnyContext(URLClassLoader cl) { + return findContextByName(cl, ""); + } + + private static Optional findContextByName(URLClassLoader cl, String contextName) { + Util.ThrowableFunction finder = (String entryName) -> { + var cls = cl.loadClass(entryName.substring(0, entryName.length() - ".class".length()).replace("/", ".")); + var parent = cls; +// var interfaces = cls.getInterfaces(); +// while (parent != Object.class && !Set.of(interfaces).contains(DbContext.class)) { + while (parent != Object.class && parent != DbContext.class) { + parent = parent.getSuperclass(); +// interfaces = cls.getInterfaces(); + } +// if (Set.of(interfaces).contains(DbContext.class)) { + if (parent == DbContext.class) { + var paramInitializer = new HashMap, Supplier>(); + paramInitializer.put(Database.class, () -> new MysqlDatabase(null, null)); + paramInitializer.put(DbContextOptions.class, () -> new DbContextOptions(null)); + + var ctors = cls.getDeclaredConstructors();//TODO create a function for initializing db contexts + if (ctors.length == 0) { + System.err.println("No constructor found in " + cls.getName()); + return null; + } + + var ctor = Arrays.stream(ctors) + .filter(e -> Arrays.stream(e.getParameterTypes()) + .allMatch(p -> Database.class.isAssignableFrom(p) || DbContextOptions.class.isAssignableFrom(p))) + .findFirst().orElse(null); + if (ctor == null) { + return null; + } + var args = Arrays.stream(ctor.getParameterTypes()).map(p -> paramInitializer.get(p).get()).toArray(); + return Util.tryGet(() -> (DbContext) ctor.newInstance(args)).orElse(null); + } + return null; + }; + return Arrays.stream(cl.getURLs()).map(url -> { + URLConnection conn; + try { + conn = url.openConnection(); + } catch (IOException ignored) { + return null; + } + if (conn instanceof JarURLConnection) { + try (var zip = new ZipInputStream(conn.getInputStream())) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + if (entry.isDirectory() || !entry.getName().endsWith(contextName + ".class")) { + continue; + } + return finder.apply(entry.getName()); + } + } catch (Throwable ignored) { + } + } else { + var search = new ArrayList(); + search.add(url); + while (search.size() > 0) { + try { + var currentUrl = search.remove(0); + conn = currentUrl.openConnection(); + try (var is = conn.getInputStream(); + var isr = new InputStreamReader(is); + var br = new BufferedReader(isr)) { + String entry; + while ((entry = br.readLine()) != null) { +// System.out.println(entry); + var newUri = currentUrl.toURI().toString(); + newUri = newUri.endsWith("/") ? newUri : newUri + "/"; + newUri += entry; + search.add(new URI(newUri).toURL()); + if (!entry.endsWith(contextName + ".class")) {//entry.isDirectory() || + continue; + } + return finder.apply(new URI(newUri).getPath().substring(url.toURI().getPath().length())); + } + } + } catch (Throwable ignored) { + } + } + } + return null; + }) + .filter(Objects::nonNull) + .findFirst(); + } + + private static String findMigrationPackageName(String dir) { + //search for java file in dir and take package name from there + var javaFiles = new File(dir).listFiles(file -> file.isFile() && file.getName().endsWith(".java")); + if (javaFiles.length > 0) { + try { + var contextFileString = Files.readAllLines(javaFiles[0].toPath()).stream().collect(Collectors.joining(" ")); + var pattern = Pattern.compile("\\s*package\\s+(.*?);"); + var matcher = pattern.matcher(contextFileString); + if (matcher.find()) { + return matcher.group(1); + } + } catch (IOException ignored) { + } + } + +// //try finding out from dir path + var path = new File(dir).getAbsolutePath(); + var i = path.indexOf("/src/"); + if (i >= 0) { + path = path.substring(i + "/src/".length()); + if (path.startsWith("main/")) { + path = path.substring("main/".length()); + if (path.startsWith("java/")) { + path = path.substring("java/".length()); + } + } + } + i = path.indexOf("/target/"); + if (i >= 0) { + path = path.substring(i + "/target/".length()); + if (path.startsWith("generated-migrations/")) { + path = path.substring("generated-migrations/".length()); + if (path.startsWith("src/")) { + path = path.substring("src/".length()); + } + } + } + return path.replace("/", "."); +// return null; + } + + public static void printHelp() { + System.out.println("migration add --cp [--context/-c ] [--output/-o ] "); + System.out.println("migration remove --cp [--context/-c ] [--output/-o ] "); + System.out.println("migration list"); + } + +// private static String findMigrationSnapshot(String dir) { +// var javaFiles = new File(dir).listFiles(file -> file.isFile() && file.getName().endsWith(".java")); +// if (javaFiles.length > 0) { +// try { +// var contextFileString = Files.readAllLines(javaFiles[0].toPath()).stream().collect(Collectors.joining(" ")); +// var pattern = Pattern.compile("\\s*package\\s+(.*?);"); +// var matcher = pattern.matcher(contextFileString); +// if (matcher.find()) { +// return matcher.group(1); +// } +// } catch (IOException ignored) { +// } +// } +// } +} diff --git a/src/main/java/jef/model/DbContext.java b/src/main/java/jef/model/DbContext.java index f3223b2..86e1f8e 100644 --- a/src/main/java/jef/model/DbContext.java +++ b/src/main/java/jef/model/DbContext.java @@ -1,7 +1,11 @@ package jef.model; +import jef.Database; +import jef.DbSet; +import jef.model.annotations.Clazz; import jef.model.constraints.ForeignKeyConstraint; import jef.serializable.SerializableObject; +import lombok.Getter; import java.lang.reflect.Field; import java.util.ArrayList; @@ -9,9 +13,40 @@ import java.util.HashMap; import java.util.Optional; import java.util.stream.Collectors; +@Getter public abstract class DbContext { private static final String ILLEGAL_CHARACTERS = "\"'`,."; + private final Database database; + private final DbContextOptions options; + + public DbContext() { + database = null; + options = null; + } + + public DbContext(Database database, DbContextOptions options) { + this.database = database; + this.options = options; + + initInitializeDbSets(); + } + + private void initInitializeDbSets() { + for (Field f : getClass().getDeclaredFields()) { + if (f.getType() != DbSet.class) { + continue; + } + f.setAccessible(true); + Clazz anno = f.getAnnotation(Clazz.class); + try { + f.set(this, new DbSet(anno.value(), f.getName()));//TODO use table name from modelbuilder + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + public void onModelCreate(ModelBuilder mb) { } diff --git a/src/main/java/jef/model/DbContextOptions.java b/src/main/java/jef/model/DbContextOptions.java index ca0c63f..3afe0f2 100644 --- a/src/main/java/jef/model/DbContextOptions.java +++ b/src/main/java/jef/model/DbContextOptions.java @@ -1,13 +1,15 @@ package jef.model; +import jef.DatabaseOptions; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @Builder -@NoArgsConstructor +@AllArgsConstructor public class DbContextOptions { + private final DatabaseOptions databaseOptions; } diff --git a/src/main/java/jef/model/DbEntity.java b/src/main/java/jef/model/DbEntity.java index 45453c7..cd951a5 100644 --- a/src/main/java/jef/model/DbEntity.java +++ b/src/main/java/jef/model/DbEntity.java @@ -27,7 +27,7 @@ import java.util.Objects; public class DbEntity { private String typeName; // @Setter -// private Class type; + private Class type; private final List> fields; private String name; private PrimaryKeyConstraint primaryKey; @@ -53,7 +53,7 @@ public class DbEntity { } DbEntity(Class type, String name, List> fields) { -// this.type = Check.notNull(type, "type"); + this.type = type; this.typeName = type.getName(); this.fields = Check.notNull(fields, "fields"); this.name = Check.notNull(name, "name"); @@ -218,7 +218,6 @@ public class DbEntity { return new DbEntityBuilder<>(mb, this); } - String extractFieldName(SerializableFunction getter) { try { var expr = new AsmParser(getter).parse().getExpression(); diff --git a/src/main/java/jef/model/DbEntityBuilder.java b/src/main/java/jef/model/DbEntityBuilder.java index f859c74..392235d 100644 --- a/src/main/java/jef/model/DbEntityBuilder.java +++ b/src/main/java/jef/model/DbEntityBuilder.java @@ -98,7 +98,7 @@ public class DbEntityBuilder { } public Optional> type() { - return Util.tryGet(() -> (Class) Class.forName(typeName())); + return Optional.ofNullable(entity.getType()).or(() -> Util.tryGet(() -> (Class) Class.forName(typeName()))); } public String typeName() { diff --git a/src/main/java/jef/model/DbField.java b/src/main/java/jef/model/DbField.java index 7716abe..e155db0 100644 --- a/src/main/java/jef/model/DbField.java +++ b/src/main/java/jef/model/DbField.java @@ -24,6 +24,7 @@ public class DbField { private DbField exposingForeignKeyOf; private String name; private boolean notNull = false; + private String sqlType; DbField(DbEntity entity, String name, String typeName) { this.entity = Check.notNull(entity, "entity"); @@ -36,7 +37,7 @@ public class DbField { } DbField(DbEntity entity, Class type, Field field) { - this(entity, type, field, field.getName()); + this(entity, type, field, Check.notNull(field, "field").getName()); } DbField(DbEntity entity, Class type, Field field, String name) { @@ -80,6 +81,10 @@ public class DbField { return this; } + public void setSqlType(String sqlType) { + this.sqlType = sqlType; + } + @Override public boolean equals(Object o) { if (!equalsCommon(o)) { diff --git a/src/main/java/jef/model/DbFieldBuilder.java b/src/main/java/jef/model/DbFieldBuilder.java index 4ff9ba9..dc2fdaf 100644 --- a/src/main/java/jef/model/DbFieldBuilder.java +++ b/src/main/java/jef/model/DbFieldBuilder.java @@ -4,7 +4,6 @@ import jef.model.constraints.IndexConstraint; import jef.model.constraints.KeyConstraint; import jef.model.constraints.UniqueKeyConstraint; import lombok.RequiredArgsConstructor; -import lombok.experimental.Accessors; import java.util.ArrayList; import java.util.List; @@ -28,6 +27,11 @@ public class DbFieldBuilder { return this; } + public DbFieldBuilder sqlType(String sqlType) { + field.setSqlType(sqlType); + return this; + } + public DbFieldBuilder isNotNull() { return isNotNull(true); } diff --git a/src/main/java/jef/model/EntityDefaultConstructorChecker.java b/src/main/java/jef/model/EntityDefaultConstructorChecker.java new file mode 100644 index 0000000..edcd5c1 --- /dev/null +++ b/src/main/java/jef/model/EntityDefaultConstructorChecker.java @@ -0,0 +1,23 @@ +package jef.model; + +import jef.util.Log; + +import java.util.Arrays; + +public class EntityDefaultConstructorChecker { + static void checkEntities(ModelBuilder mb) { + for (DbEntityBuilder entity : mb.entities()) { + checkEntity(entity); + } + } + + static void checkEntity(DbEntityBuilder entity) { + Log.debug("Checking default constructor exists for entity '" + entity.name() + "' of type " + entity.className()); + + //check no arg constructor + Class clazz = entity.type().orElseThrow(); + if (Arrays.stream(clazz.getDeclaredConstructors()).noneMatch(e -> e.getParameterCount() == 0)) { + throw new ModelException("Class '" + clazz.getSimpleName() + "' does not have a default constructor!"); + } + } +} diff --git a/src/main/java/jef/model/ModelBuilder.java b/src/main/java/jef/model/ModelBuilder.java index 05fd1e9..d3e60ee 100644 --- a/src/main/java/jef/model/ModelBuilder.java +++ b/src/main/java/jef/model/ModelBuilder.java @@ -1,16 +1,13 @@ package jef.model; +import jef.Database; import jef.model.annotations.processors.AnnotationProcessor; -import jef.model.annotations.processors.ForeignKeyProcessor; -import jef.model.annotations.processors.IndexProcessor; -import jef.model.annotations.processors.KeyProcessor; -import jef.model.annotations.processors.NotNullProcessor; -import jef.model.annotations.processors.UniqueProcessor; import jef.model.constraints.ForeignKeyConstraint; import jef.model.constraints.IndexConstraint; import jef.model.constraints.KeyConstraint; import jef.model.constraints.PrimaryKeyConstraint; import jef.model.constraints.UniqueKeyConstraint; +import jef.mysql.MysqlDatabase; import jef.serializable.SerializableObject; import jef.util.Check; import jef.util.Util; @@ -47,9 +44,10 @@ public class ModelBuilder { } } - private static ModelBuilder from0(Class context, ModelBuilderOptions options) throws Throwable { + private static ModelBuilder from0(Class context, ModelBuilderOptions options) { var mb = new ModelBuilder(new ArrayList<>()); EntityInitializer.initEntities(mb, context); + EntityDefaultConstructorChecker.checkEntities(mb); PrimaryKeyInitializer.initPrimaryKeys(mb); ForeignKeyExposeInitializer.initForeignKeyExposures(mb); ForeignKeyInitializer.initForeignKeys(mb); @@ -58,20 +56,25 @@ public class ModelBuilder { processor.apply(mb); } - Util.ThrowableFunction init; + Util.ThrowableBiFunction init;//TODO create a function for initializing db contexts try { - var ctor = context.getDeclaredConstructor(DbContextOptions.class); + var ctor = context.getDeclaredConstructor(Database.class, DbContextOptions.class); init = ctor::newInstance; } catch (NoSuchMethodException e) { try { - var ctor = context.getDeclaredConstructor(); - init = o -> ctor.newInstance(); + var ctor = context.getDeclaredConstructor(DbContextOptions.class); + init = (d, o) -> ctor.newInstance(o); } catch (NoSuchMethodException e2) { - throw new RuntimeException(e); + try { + var ctor = context.getDeclaredConstructor(); + init = (d, o) -> ctor.newInstance(); + } catch (NoSuchMethodException e3) { + throw new RuntimeException(e); + } } } try { - DbContext instance = init.apply(options.getContextOptions()); + DbContext instance = init.apply(new MysqlDatabase(null, null), options.getContextOptions());//TODO instance.onModelCreate(mb); instance.onModelValidate(mb); } catch (Throwable e) { @@ -143,7 +146,12 @@ public class ModelBuilder { */ public DbEntityBuilder entity(Class clazz) { Check.notNull(clazz, "clazz"); - return entity(clazz.getName()); + var entity = (DbEntity) getEntity(clazz); + if (entity == null) { + entity = new DbEntity<>(clazz); + entities.add(entity); + } + return new DbEntityBuilder<>(this, entity); } /** @@ -199,6 +207,7 @@ public class ModelBuilder { var nf = new DbField(entity, e.getName(), e.getTypeName()); nf.setField(e.getField()); nf.setType(e.getType()); + nf.setSqlType(e.getSqlType()); nf.setNotNull(e.isNotNull()); nf.setModelField(e.isModelField()); nf.setDatabaseField(e.isDatabaseField()); diff --git a/src/main/java/jef/model/ModelBuilderOptions.java b/src/main/java/jef/model/ModelBuilderOptions.java index 70a880d..363ddfa 100644 --- a/src/main/java/jef/model/ModelBuilderOptions.java +++ b/src/main/java/jef/model/ModelBuilderOptions.java @@ -30,6 +30,6 @@ public class ModelBuilderOptions { KeyProcessor.INSTANCE, ForeignKeyProcessor.INSTANCE )); - this.contextOptions = new DbContextOptions(); + this.contextOptions = new DbContextOptions(null);//TODO } } diff --git a/src/main/java/jef/model/PrimaryKeyInitializer.java b/src/main/java/jef/model/PrimaryKeyInitializer.java index 6cd40aa..a674788 100644 --- a/src/main/java/jef/model/PrimaryKeyInitializer.java +++ b/src/main/java/jef/model/PrimaryKeyInitializer.java @@ -16,7 +16,7 @@ class PrimaryKeyInitializer { } static void initPrimaryKeys(ModelBuilder mb, DbEntityBuilder entity) { - var fields = ReflectionUtil.getFieldsRecursive(entity.type().get()); + var fields = ReflectionUtil.getFieldsRecursive(entity.type().orElseThrow()); var idFields = new ArrayList(); //search for fields with @Id annotation diff --git a/src/main/java/jef/model/SqlTypeMapper.java b/src/main/java/jef/model/SqlTypeMapper.java new file mode 100644 index 0000000..f9449f7 --- /dev/null +++ b/src/main/java/jef/model/SqlTypeMapper.java @@ -0,0 +1,21 @@ +package jef.model; + +import java.util.Optional; + +public class SqlTypeMapper { + public Optional map(String typeName) { + if (typeName == null) { + return Optional.empty(); + } + return Optional.ofNullable(switch (typeName) { + case "java.lang.String" -> "VARCHAR(255)";//TODO add length and precision as param + case "int" -> "INT(11)"; + case "float" -> "FLOAT"; + case "double" -> "DOUBLE"; + case "boolean" -> "INT(11)"; + case "short" -> "INT(11)"; + case "long" -> "BIGINT"; + default -> null; + }); + } +} diff --git a/src/main/java/jef/model/migration/MigrationBuilder.java b/src/main/java/jef/model/migration/MigrationBuilder.java index f098e92..4253621 100644 --- a/src/main/java/jef/model/migration/MigrationBuilder.java +++ b/src/main/java/jef/model/migration/MigrationBuilder.java @@ -15,10 +15,12 @@ import jef.model.migration.operation.MigrationOperation; import jef.model.migration.operation.RenameFieldOperation; import jef.model.migration.operation.RenameTableOperation; import jef.model.migration.operation.UpdateFieldOperation; +import lombok.Getter; import java.util.ArrayList; import java.util.List; +@Getter public class MigrationBuilder { private final List> operations = new ArrayList<>(); diff --git a/src/main/java/jef/model/migration/creator/MigrationBuilderGenerator.java b/src/main/java/jef/model/migration/creator/MigrationBuilderGenerator.java index bb040fa..467f266 100644 --- a/src/main/java/jef/model/migration/creator/MigrationBuilderGenerator.java +++ b/src/main/java/jef/model/migration/creator/MigrationBuilderGenerator.java @@ -58,19 +58,21 @@ public class MigrationBuilderGenerator { //generate migration class file var normalImports = imports.stream().filter(e -> !e.getName().startsWith("java")).sorted(Comparator.comparing(Class::getName, String.CASE_INSENSITIVE_ORDER)).toList(); var javaImports = imports.stream().filter(e -> e.getName().startsWith("java")).sorted(Comparator.comparing(Class::getName, String.CASE_INSENSITIVE_ORDER)).toList(); - var java = "package " + packageName + ";\n" - + "\n" - + normalImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")) + "\n\n" - + javaImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")) + "\n\n" - + "public class " + name + " implements Migration {\n" - + " public void up(MigrationBuilder mb) {\n" - + " " + migrationUp.replace("\n", "\n ") + "\n" - + " }\n" - + "\n" - + " public void down(MigrationBuilder mb) {\n" - + " " + migrationDown.replace("\n", "\n ") + "\n" - + " }\n" - + "}\n"; + var normalImportsString = normalImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")); + var javaImportsString = javaImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")); + + var java = (packageName != null ? "package " + packageName + ";\n\n" : "") + + normalImportsString + (normalImportsString.length() > 0 ? "\n\n" : "") + + javaImportsString + (javaImportsString.length() > 0 ? "\n\n" : "") + + "public class " + name + " implements Migration {\n" + + " public void up(MigrationBuilder mb) {\n" + + " " + migrationUp.replace("\n", "\n ") + "\n" + + " }\n" + + "\n" + + " public void down(MigrationBuilder mb) {\n" + + " " + migrationDown.replace("\n", "\n ") + "\n" + + " }\n" + + "}\n"; return java; } @@ -99,8 +101,7 @@ public class MigrationBuilderGenerator { } private String getMigrationJava(MigrationOperation migrationOperation) { - var mapper = ((Function) - OP_TO_STRING_MAPPERS.getOrDefault(migrationOperation.getClass(), UNSUPPORTED_MIGRATION_OPERATION_FUNCTION)); + var mapper = (Function) OP_TO_STRING_MAPPERS.getOrDefault(migrationOperation.getClass(), UNSUPPORTED_MIGRATION_OPERATION_FUNCTION); return mapper.apply(migrationOperation); } diff --git a/src/main/java/jef/model/migration/creator/MigrationCreator.java b/src/main/java/jef/model/migration/creator/MigrationCreator.java index 74a1a24..bdcac89 100644 --- a/src/main/java/jef/model/migration/creator/MigrationCreator.java +++ b/src/main/java/jef/model/migration/creator/MigrationCreator.java @@ -2,7 +2,7 @@ package jef.model.migration.creator; import jef.model.DbField; import jef.model.ModelBuilder; -import jef.model.ModelException; +import jef.model.SqlTypeMapper; import jef.model.constraints.ForeignKeyConstraint; import jef.model.constraints.IndexConstraint; import jef.model.constraints.KeyConstraint; @@ -30,11 +30,10 @@ import lombok.Setter; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class MigrationCreator { - - public Result createMigration(ModelBuilder from, ModelBuilder to, String name, String packageName, String currentSnapshotJava) { var result = new Result(); @@ -66,7 +65,7 @@ public class MigrationCreator { } private String generateModelBuilderJava(ModelBuilder mb, String name, String packageName) { - return new ModelBuilderGenerator(mb, name, packageName).generate().getJava(); + return new ModelBuilderGenerator(mb, name, packageName, new SqlTypeMapper()).generate().getJava();//TODO mapper } private Result generateMigration(ModelBuilder from, ModelBuilder to, String name, String packageName, Result result) { @@ -128,13 +127,17 @@ public class MigrationCreator { .filter(DbField::isDatabaseField) .map(e -> new AddFieldOperation.Builder(toEntity.getName(), e.getName()) .notNull(e.isNotNull()) - .sqlType("TODO")) + .sqlType(getSqlType(e))) .toList() )); } } } + private String getSqlType(DbField e) { + return Optional.ofNullable(e.getSqlType()).or(() -> new SqlTypeMapper().map(e.getTypeName())).orElse(null); + } + private void addTableRenameGeneration(ModelBuilder fromReduced, ModelBuilder toReduced, ModelBuilder from, ModelBuilder to, List steps) { for (var toEntity : toReduced.getEntities()) { var fromEntity = fromReduced.getEntity(toEntity.getTypeName()); diff --git a/src/main/java/jef/model/migration/creator/ModelBuilderGenerator.java b/src/main/java/jef/model/migration/creator/ModelBuilderGenerator.java index d9a7875..8e95219 100644 --- a/src/main/java/jef/model/migration/creator/ModelBuilderGenerator.java +++ b/src/main/java/jef/model/migration/creator/ModelBuilderGenerator.java @@ -5,6 +5,7 @@ import jef.model.DbEntity; import jef.model.DbEntityBuilder; import jef.model.DbField; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.constraints.ForeignKeyConstraint; import jef.model.constraints.IndexConstraint; import jef.model.constraints.KeyConstraint; @@ -17,6 +18,7 @@ import lombok.RequiredArgsConstructor; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -25,6 +27,7 @@ public class ModelBuilderGenerator { private final ModelBuilder mb; private final String name; private final String packageName; + private final SqlTypeMapper sqlTypeMapper; private final Set> imports = new HashSet<>(); @Getter @@ -50,13 +53,14 @@ public class ModelBuilderGenerator { + indent + "DbEntityBuilder referencedEntity;\n"; for (DbEntity entity : mb.getEntities()) { java += indent + "mb.entity(\"" + entity.getTypeName() + "\")\n" - + indent + " .name(\"" + entity.getName() + "\");\n"; + + indent + " .name(\"" + entity.getName() + "\");\n"; for (DbField field : entity.getFields()) { java += indent + "mb.entity(\"" + entity.getTypeName() + "\")\n" + indent + " .field(\"" + field.getName() + "\", \"" + field.getTypeName() + "\")" + + "\n" + indent + " .sqlType(" + getSqlType(field) + ")" + (field.isNotNull() ? "\n" + indent + " .isNotNull()" : "") + "\n" + indent + " .isDatabaseField(" + field.isDatabaseField() + ")" - + "\n" + indent + " .isModelField(" + field.isModelField() + ");"; + + "\n" + indent + " .isModelField(" + field.isModelField() + ");\n"; } if (entity.getPrimaryKey() != null) { imports.add(List.class); @@ -105,10 +109,14 @@ public class ModelBuilderGenerator { var javaImports = imports.stream().filter(e -> e.getName().startsWith("java")).sorted(Comparator.comparing(Class::getName, String.CASE_INSENSITIVE_ORDER)).toList(); //finalize - java = "package " + packageName + ";\n\n" + java = (packageName != null ? "package " + packageName + ";\n\n" : "") + normalImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")) + "\n\n" + javaImports.stream().map(e -> "import " + e.getName().replace("$", ".") + ";").collect(Collectors.joining("\n")) + "\n\n" + java; return java; } + + private String getSqlType(DbField f) { + return Optional.ofNullable(f.getSqlType()).or(() -> sqlTypeMapper.map(f.getTypeName())).map(e -> "\"" + e + "\"").orElse(null); + } } diff --git a/src/main/java/jef/mysql/MysqlDatabase.java b/src/main/java/jef/mysql/MysqlDatabase.java new file mode 100644 index 0000000..9e08991 --- /dev/null +++ b/src/main/java/jef/mysql/MysqlDatabase.java @@ -0,0 +1,19 @@ +package jef.mysql; + +import jef.Database; +import jef.DatabaseOptions; +import jef.MigrationException; +import jef.mysql.migration.MysqlMigrationApplier; + +import java.sql.Connection; + +public class MysqlDatabase extends Database { + public MysqlDatabase(Connection connection, DatabaseOptions options) { + super(connection, options); + } + + @Override + public void migrate() throws MigrationException { + new MysqlMigrationApplier(connection, options).migrate(); + } +} diff --git a/src/main/java/jef/mysql/migration/MigrationApplier.java b/src/main/java/jef/mysql/migration/MigrationApplier.java new file mode 100644 index 0000000..0dc1f0f --- /dev/null +++ b/src/main/java/jef/mysql/migration/MigrationApplier.java @@ -0,0 +1,109 @@ +package jef.mysql.migration; + +import jef.DatabaseOptions; +import jef.MigrationException; +import jef.model.migration.Migration; +import jef.model.migration.MigrationBuilder; +import jef.model.migration.operation.MigrationOperation; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@AllArgsConstructor +@Getter +public abstract class MigrationApplier { + protected final Connection connection; + protected final DatabaseOptions options; + + public void migrate() throws MigrationException { + var migrations = findMigrations(options.getMigrationsPackage()); //TODO find all migrations, support multiple db contexts + try { + if (migrationsTableExists()) { + getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a))); + } else { + createMigrationsTable(); + } + for (Migration migration : migrations) { + applyMigration(migration); + } + } catch (SQLException e) { + throw new MigrationException(e); + } + } + + protected void applyMigration(Migration m) throws MigrationException { + var mb = new MigrationBuilder(); + m.up(mb); + var operations = mb.getOperations().stream().map(MigrationOperation.Builder::build).toList(); + + try { + connection.setAutoCommit(false); + try { + for (MigrationOperation op : operations) { + var stmt = MysqlMigrationOperationTranslator.translate(connection, op); + try { + stmt.execute(); + } catch (SQLException e) { + throw new SQLException("Failed to execute query: " + stmt, e); + } finally { + try { + stmt.close(); + } catch (SQLException ignored) { + } + } + } + insertMigrationLog(m); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } finally { + connection.setAutoCommit(true); + } + } catch (SQLException e) { + throw new MigrationException("Failed to apply migration '" + m.getClass().getSimpleName() + "' to the database: " + e.getLocalizedMessage(), e); + } + } + + protected List findMigrations(String packageName) throws MigrationException { + try (var is = getClass().getClassLoader().getResourceAsStream(packageName.replace(".", "/")); + var reader = new BufferedReader(new InputStreamReader(is))) { + return reader.lines() + .filter(line -> line.endsWith(".class")) + .map(line -> line.substring(0, line.length() - ".class".length())) + .map(line -> { + try { + var cls = getClass().getClassLoader().loadClass(packageName + (packageName.isEmpty() ? "" : ".") + line); + if (!Migration.class.isAssignableFrom(cls)) { + return null; + } + return (Migration) cls.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } catch (Exception e) { + if (e instanceof RuntimeException re) { + e = re; + } + throw new MigrationException("Error while looking up migrations in package '" + packageName + "'", e); + } + } + + protected abstract boolean migrationsTableExists() throws SQLException; + + protected abstract void createMigrationsTable() throws SQLException; + + protected abstract List getAppliedMigrations() throws SQLException; + + protected abstract void insertMigrationLog(Migration m) throws SQLException; +} diff --git a/src/main/java/jef/mysql/migration/MysqlMigrationApplier.java b/src/main/java/jef/mysql/migration/MysqlMigrationApplier.java new file mode 100644 index 0000000..61ed3cd --- /dev/null +++ b/src/main/java/jef/mysql/migration/MysqlMigrationApplier.java @@ -0,0 +1,76 @@ +package jef.mysql.migration; + +import jef.DatabaseOptions; +import jef.model.migration.Migration; +import jef.model.migration.operation.AddFieldOperation; +import jef.model.migration.operation.AddTableOperation; +import jef.model.migration.operation.AddUniqueKeyOperation; +import jef.model.migration.operation.MigrationOperation; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class MysqlMigrationApplier extends MigrationApplier { + public MysqlMigrationApplier(Connection connection, DatabaseOptions options) { + super(connection, options); + } + + @Override + protected void insertMigrationLog(Migration m) throws SQLException { + try (var stmt = connection.prepareStatement("INSERT INTO `__jef_migration_log` (`name`, `version`) VALUES (?, ?)")) {//TODO configurable log table name + stmt.setString(1, m.getClass().getSimpleName()); + stmt.setString(2, "0.1"); //TODO insert actual library version + stmt.executeUpdate(); + } + } + + @Override + protected boolean migrationsTableExists() throws SQLException { + try (var stmt = connection.prepareStatement("SHOW TABLES"); + var res = stmt.executeQuery()) { + while (res.next()) { + if (res.getString(1).equals("__jef_migration_log")) { + return true; + } + } + } + return false; + } + + @Override + protected void createMigrationsTable() throws SQLException { + var table = "__jef_migration_log"; + var ops = List.of( + new AddTableOperation.Builder(table, List.of( + new AddFieldOperation.Builder(table, "name").notNull(true).sqlType("VARCHAR(255)"), + new AddFieldOperation.Builder(table, "version").notNull(true).sqlType("VARCHAR(255)") + )), + new AddUniqueKeyOperation.Builder("U_" + table + "_name", table, List.of("name")) + ); + for (MigrationOperation.Builder e : ops) { + try (var stmt = MysqlMigrationOperationTranslator.translate(connection, e.build())) { + stmt.executeUpdate(); + } + } + } + + @Override + protected List getAppliedMigrations() throws SQLException { + var ret = new ArrayList(); + if (!migrationsTableExists()) { + return ret; + } + + try (var stmt = connection.prepareStatement("SELECT `name` FROM `__jef_migration_log`"); + var res = stmt.executeQuery()) { + while (res.next()) { + if (res.getString(1).equals("__jef_migration_log")) { + ret.add(res.getString("name")); + } + } + } + return ret; + } +} diff --git a/src/main/java/jef/mysql/migration/MysqlMigrationOperationTranslator.java b/src/main/java/jef/mysql/migration/MysqlMigrationOperationTranslator.java new file mode 100644 index 0000000..cb5093d --- /dev/null +++ b/src/main/java/jef/mysql/migration/MysqlMigrationOperationTranslator.java @@ -0,0 +1,149 @@ +package jef.mysql.migration; + +import jef.model.migration.operation.AddFieldOperation; +import jef.model.migration.operation.AddForeignKeyOperation; +import jef.model.migration.operation.AddIndexOperation; +import jef.model.migration.operation.AddKeyOperation; +import jef.model.migration.operation.AddPrimaryKeyOperation; +import jef.model.migration.operation.AddTableOperation; +import jef.model.migration.operation.AddUniqueKeyOperation; +import jef.model.migration.operation.DropConstraintOperation; +import jef.model.migration.operation.DropFieldOperation; +import jef.model.migration.operation.DropTableOperation; +import jef.model.migration.operation.MigrationOperation; +import jef.model.migration.operation.RenameFieldOperation; +import jef.model.migration.operation.RenameTableOperation; +import jef.model.migration.operation.UpdateFieldOperation; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class MysqlMigrationOperationTranslator { + private static final Map, Mapper> translators; + + static { + var map = new HashMap, Mapper>(); + map.put(AddFieldOperation.class, MysqlMigrationOperationTranslator::translateAddFieldOperation); + map.put(AddForeignKeyOperation.class, MysqlMigrationOperationTranslator::translateAddForeignKeyOperation); + map.put(AddIndexOperation.class, MysqlMigrationOperationTranslator::translateAddIndexOperation); + map.put(AddKeyOperation.class, MysqlMigrationOperationTranslator::translateAddKeyOperation); + map.put(AddPrimaryKeyOperation.class, MysqlMigrationOperationTranslator::translateAddPrimaryKeyOperation); + map.put(AddTableOperation.class, MysqlMigrationOperationTranslator::translateAddTableOperation); + map.put(AddUniqueKeyOperation.class, MysqlMigrationOperationTranslator::translateAddUniqueKeyOperation); + map.put(DropConstraintOperation.class, MysqlMigrationOperationTranslator::translateDropConstraintOperation); + map.put(DropFieldOperation.class, MysqlMigrationOperationTranslator::translateDropFieldOperation); + map.put(DropTableOperation.class, MysqlMigrationOperationTranslator::translateDropTableOperation); + map.put(RenameFieldOperation.class, MysqlMigrationOperationTranslator::translateRenameFieldOperation); + map.put(RenameTableOperation.class, MysqlMigrationOperationTranslator::translateRenameTableOperation); + map.put(UpdateFieldOperation.class, MysqlMigrationOperationTranslator::translateUpdateFieldOperation); + translators = Collections.unmodifiableMap(map); + } + + public static PreparedStatement translate(Connection connection, MigrationOperation operation) throws SQLException { + return Optional.ofNullable(translators.get(operation.getClass())).orElseThrow().apply(connection, operation); + } + + private static PreparedStatement translateAddFieldOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddFieldOperation op = (AddFieldOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD COLUMN `" + op.getField() + "` " + + op.getSqlType() + (op.isNotNull() ? " NOT NULL" : "") //TODO add after field specification + + " LAST"); + } + + private static PreparedStatement translateAddForeignKeyOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddForeignKeyOperation op = (AddForeignKeyOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD CONSTRAINT `" + op.getName() + "`" + + " FOREIGN KEY (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")" + + " REFERENCES `" + op.getReferencedTable() + "`(" + op.getReferencedFields().stream() + .map(e -> "`" + e + "`") + .collect(Collectors.joining(", ")) + ")"); + } + + private static PreparedStatement translateAddIndexOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddIndexOperation op = (AddIndexOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD CONSTRAINT `" + op.getName() + "`" + + " INDEX (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")"); + } + + private static PreparedStatement translateAddKeyOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddKeyOperation op = (AddKeyOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD CONSTRAINT `" + op.getName() + "`" + + " KEY (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")"); + } + + private static PreparedStatement translateAddPrimaryKeyOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddPrimaryKeyOperation op = (AddPrimaryKeyOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD " + (!op.getName().equals("PRIMARY") ? "CONSTRAINT `" + op.getName() + "`" : "") + + " PRIMARY KEY (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")"); +// + " ADD CONSTRAINT `" + op.getName() + "`" +// + " PRIMARY KEY (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")"); + } + + private static PreparedStatement translateAddTableOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddTableOperation op = (AddTableOperation) operation; + return connection.prepareStatement("CREATE TABLE `" + op.getTable() + "` (" + + op.getFields().stream().map(e -> { + var f = e.build(); + return "`" + f.getField() + "` " + f.getSqlType() + (f.isNotNull() ? " NOT NULL" : ""); + }).collect(Collectors.joining(", ")) + + ")"); //TODO default collocation from database config or operation, field collation, primary key, constraints + } + + private static PreparedStatement translateAddUniqueKeyOperation(Connection connection, MigrationOperation operation) throws SQLException { + AddUniqueKeyOperation op = (AddUniqueKeyOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ADD CONSTRAINT `" + op.getName() + "`" + + " UNIQUE (" + op.getFields().stream().map(e -> "`" + e + "`").collect(Collectors.joining(", ")) + ")"); + } + + private static PreparedStatement translateDropConstraintOperation(Connection connection, MigrationOperation operation) throws SQLException { + DropConstraintOperation op = (DropConstraintOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " DROP CONSTRAINT `" + op.getName() + "`"); + } + + private static PreparedStatement translateDropFieldOperation(Connection connection, MigrationOperation operation) throws SQLException { + DropFieldOperation op = (DropFieldOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " DROP COLUMN `" + op.getField() + "`"); + } + + private static PreparedStatement translateDropTableOperation(Connection connection, MigrationOperation operation) throws SQLException { + DropTableOperation op = (DropTableOperation) operation; + return connection.prepareStatement("DROP TABLE `" + op.getTable() + "`"); + } + + private static PreparedStatement translateRenameFieldOperation(Connection connection, MigrationOperation operation) throws SQLException { + RenameFieldOperation op = (RenameFieldOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " RENAME COLUMN `" + op.getOldName() + "` `" + op.getNewName() + "`"); + } + + private static PreparedStatement translateRenameTableOperation(Connection connection, MigrationOperation operation) throws SQLException { + RenameTableOperation op = (RenameTableOperation) operation; + return connection.prepareStatement("RENAME TABLE `" + op.getOldName() + "` `" + op.getNewName() + "`"); + } + + private static PreparedStatement translateUpdateFieldOperation(Connection connection, MigrationOperation operation) throws SQLException { + UpdateFieldOperation op = (UpdateFieldOperation) operation; + return connection.prepareStatement("ALTER TABLE `" + op.getTable() + "`" + + " ALTER COLUMN `" + op.getField() + "` `" + op.getNewName() + "`" + + op.getSqlType() + (op.isNotNull() ? " NOT NULL" : "")); + } + + @FunctionalInterface + private interface Mapper { + PreparedStatement apply(Connection connection, T operation) throws SQLException; + } +} diff --git a/src/main/java/jef/util/Util.java b/src/main/java/jef/util/Util.java index 7a2255c..d74a4cc 100644 --- a/src/main/java/jef/util/Util.java +++ b/src/main/java/jef/util/Util.java @@ -20,4 +20,8 @@ public abstract class Util { public interface ThrowableFunction { R apply(T t) throws Throwable; } + @FunctionalInterface + public interface ThrowableBiFunction { + R apply(T t, U u) throws Throwable; + } } diff --git a/src/test/java/jef/asm/OptimizedAsmParserTest.java b/src/test/java/jef/asm/OptimizedAsmParserTest.java index 826079b..da84411 100644 --- a/src/test/java/jef/asm/OptimizedAsmParserTest.java +++ b/src/test/java/jef/asm/OptimizedAsmParserTest.java @@ -150,6 +150,10 @@ public class OptimizedAsmParserTest { act = new OptimizedAsmParser((SerializablePredicate) (TestClass e) -> e.l == 0L || e.l == 1L).parse().getExpression().toString(); Assertions.assertEquals("`l` = 0 OR `l` = 1", act); +// +// +// act = new OptimizedAsmParser((SerializablePredicate) (TestClass e) -> e.b == false || e.b == true).parse().getExpression().toString(); +// Assertions.assertEquals("`b` = 0 OR `b` = 1", act); } @Test diff --git a/src/test/java/jef/model/EntityDefaultConstructorCheckerTest.java b/src/test/java/jef/model/EntityDefaultConstructorCheckerTest.java new file mode 100644 index 0000000..848c297 --- /dev/null +++ b/src/test/java/jef/model/EntityDefaultConstructorCheckerTest.java @@ -0,0 +1,39 @@ +package jef.model; + +import jef.model.annotations.Id; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EntityDefaultConstructorCheckerTest { + @Test + public void test0ArgConstructor() { + var mb = new ModelBuilder(); + EntityDefaultConstructorChecker.checkEntity(mb.entity(TestClass0Arg.class)); + assertTrue(true); + } + + @Test + public void testNon0ArgConstructor() { + var mb = new ModelBuilder(); + var ex = assertThrows(ModelException.class, () -> EntityDefaultConstructorChecker.checkEntity(mb.entity(TestClassNon0Arg.class))); + assertEquals("Class 'TestClassNon0Arg' does not have a default constructor!", ex.getMessage()); + } + + @NoArgsConstructor + public static class TestClass0Arg extends SerializableObject { + @Id + public int i = 1; + } + + @AllArgsConstructor + public static class TestClassNon0Arg extends SerializableObject { + @Id + public int i = 1; + } +} \ No newline at end of file diff --git a/src/test/java/jef/model/ModelBuilderSimpleTest.java b/src/test/java/jef/model/ModelBuilderSimpleTest.java index 3150e53..7ef0ae7 100644 --- a/src/test/java/jef/model/ModelBuilderSimpleTest.java +++ b/src/test/java/jef/model/ModelBuilderSimpleTest.java @@ -34,5 +34,4 @@ class ModelBuilderSimpleTest { public float f; public long l; } - } \ No newline at end of file diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddEntityTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddEntityTest.java index 466ede0..2072942 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddEntityTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddEntityTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.constraints.ForeignKeyConstraint; @@ -31,7 +32,7 @@ public class MigrationCreatorAddEntityTest extends MigrationCreatorTestBase { to.getEntity("AddedEntity").setPrimaryKey(new PrimaryKeyConstraint(to.getEntity("AddedEntity"), List.of(to.getEntity("AddedEntity").getField("id")))); to.getEntity("AddedEntity").addForeignKey(new ForeignKeyConstraint(to.getEntity("AddedEntity"), List.of(to.getEntity("AddedEntity").getField("addedField")), to.getEntity("AddedEntity"), List.of(to.getEntity("AddedEntity").getField("id")), ForeignKeyConstraint.Action.CASCADE, ForeignKeyConstraint.Action.CASCADE)); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddFieldTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddFieldTest.java index cc355ec..bd5d30f 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddFieldTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddFieldTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.constraints.ForeignKeyConstraint; @@ -27,7 +28,7 @@ public class MigrationCreatorAddFieldTest extends MigrationCreatorTestBase{ ent.getOrCreateField("addedField", int.class.getName()); ent.addForeignKey(new ForeignKeyConstraint(ent, List.of(ent.getField("addedField")), ent, List.of(ent.getField("i")), ForeignKeyConstraint.Action.CASCADE, ForeignKeyConstraint.Action.CASCADE)); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddForeignKeyTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddForeignKeyTest.java index 9b96948..538416f 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddForeignKeyTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddForeignKeyTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.constraints.ForeignKeyConstraint; @@ -24,7 +25,7 @@ public class MigrationCreatorAddForeignKeyTest extends MigrationCreatorTestBase var ent = to.getEntity(TestClass.class); ent.addForeignKey(new ForeignKeyConstraint(ent, List.of(ent.getField("i2")), ent, List.of(ent.getField("i")), ForeignKeyConstraint.Action.CASCADE, ForeignKeyConstraint.Action.CASCADE)); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddIndexTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddIndexTest.java index f77a59b..b4580d2 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddIndexTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddIndexTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.AddIndexOperation; @@ -23,7 +24,7 @@ public class MigrationCreatorAddIndexTest extends MigrationCreatorTestBase{ var ent = to.entity(TestClass.class); ent.field("d", double.class.getName()).isIndex(true); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddKeyTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddKeyTest.java index d251c9e..835d342 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddKeyTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddKeyTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.AddKeyOperation; @@ -23,7 +24,7 @@ public class MigrationCreatorAddKeyTest extends MigrationCreatorTestBase{ var ent = to.entity(TestClass.class); ent.field("d", double.class.getName()).isKey(true); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorAddUniqueTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorAddUniqueTest.java index 0c3ddbb..980a018 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorAddUniqueTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorAddUniqueTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.AddUniqueKeyOperation; @@ -23,7 +24,7 @@ public class MigrationCreatorAddUniqueTest extends MigrationCreatorTestBase{ var ent = to.entity(TestClass.class); ent.field("d", double.class.getName()).isUnique(true); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorEmptyTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorEmptyTest.java index 6f4e9f4..a5871e4 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorEmptyTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorEmptyTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.annotations.Index; @@ -22,7 +23,7 @@ public class MigrationCreatorEmptyTest extends MigrationCreatorTestBase{ var from = ModelBuilder.from(Ctx.class); var to = ModelBuilder.from(Ctx.class); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "EmptyMigration", "test",new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "EmptyMigration", "test",new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { assertEquals(0, res.getStepsUp().size()); assertEquals(0, res.getStepsDown().size()); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorInitialMigrationTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorInitialMigrationTest.java index 5affeb4..a6cb504 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorInitialMigrationTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorInitialMigrationTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.AddForeignKeyOperation; @@ -24,7 +25,7 @@ public class MigrationCreatorInitialMigrationTest extends MigrationCreatorTestBa var from = new ModelBuilder(List.of()); var to = ModelBuilder.from(Ctx.class); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "InitialMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "InitialMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameEntityTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameEntityTest.java index c3e0a98..b92bdfd 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameEntityTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameEntityTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.AddForeignKeyOperation; @@ -25,7 +26,7 @@ public class MigrationCreatorRenameEntityTest extends MigrationCreatorTestBase { var ent = to.entity(TestClass.class); ent.name("d2"); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldConstraintsTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldConstraintsTest.java index 9d4bb23..1e6a82d 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldConstraintsTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldConstraintsTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.ForeignKey; import jef.model.annotations.Id; @@ -31,7 +32,7 @@ public class MigrationCreatorRenameFieldConstraintsTest extends MigrationCreator var to = ModelBuilder.from(Ctx.class); to.getEntity(TestClass.class).getField("i").setName("i2"); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldTest.java b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldTest.java index db8abde..8911f19 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldTest.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorRenameFieldTest.java @@ -3,6 +3,7 @@ package jef.model.migration.creator; import jef.DbSet; import jef.model.DbContext; import jef.model.ModelBuilder; +import jef.model.SqlTypeMapper; import jef.model.annotations.Clazz; import jef.model.annotations.Id; import jef.model.migration.operation.RenameFieldOperation; @@ -22,7 +23,7 @@ public class MigrationCreatorRenameFieldTest extends MigrationCreatorTestBase { var ent = to.entity(TestClass.class); ent.field("d", double.class.getName()).name("d2"); var mc = new MigrationCreator(); - var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test").generate().getJava()); + var res = mc.createMigration(from, to, "SomeMigration", "test", new ModelBuilderGenerator(from, "Current", "test", new SqlTypeMapper()).generate().getJava());//TODO mapper try { validateUp(res); validateDown(res); diff --git a/src/test/java/jef/model/migration/creator/MigrationCreatorTestBase.java b/src/test/java/jef/model/migration/creator/MigrationCreatorTestBase.java index 8a2a4f5..2b1dab6 100644 --- a/src/test/java/jef/model/migration/creator/MigrationCreatorTestBase.java +++ b/src/test/java/jef/model/migration/creator/MigrationCreatorTestBase.java @@ -18,8 +18,8 @@ import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; public class MigrationCreatorTestBase { - private static final File JAVA_FILES_DIR = new File("target/jef-tests/sources"); - private static final File CLASS_FILES_DIR = new File("target/jef-tests/classes"); + private static final File JAVA_FILES_DIR = new File("target/test-generated-migrations/src");//TODO move tot a TESTUTil class or smth + private static final File CLASS_FILES_DIR = new File("target/test-generated-migrations/target"); public void validateMigration(MigrationCreator.Result result, ModelBuilder from, ModelBuilder to) { validateMigrationJava(result.getMigration()); diff --git a/src/test/java/jef/mysql/migration/MysqlMigrationTest.java b/src/test/java/jef/mysql/migration/MysqlMigrationTest.java new file mode 100644 index 0000000..42f7f3f --- /dev/null +++ b/src/test/java/jef/mysql/migration/MysqlMigrationTest.java @@ -0,0 +1,157 @@ +package jef.mysql.migration; + +import jef.Database; +import jef.DatabaseOptions; +import jef.DbSet; +import jef.model.DbContext; +import jef.model.DbContextOptions; +import jef.model.annotations.Clazz; +import jef.model.annotations.ForeignKey; +import jef.mysql.MysqlDatabase; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.sql.DriverManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MysqlMigrationTest { + private static final String GENERATE_MIGRATIONS_FOLDER = "target/test-generated-migrations/"; + private static final String GENERATE_MIGRATIONS_FOLDER_SRC = GENERATE_MIGRATIONS_FOLDER + "src/"; + private static final String GENERATE_MIGRATIONS_FOLDER_TARGET = GENERATE_MIGRATIONS_FOLDER + "target/"; + + @Test + public void test() throws Exception { + clearMigrationFolders(); + generateInitialMigration(); + compileInitialMigration(); + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + var dboptions = new DatabaseOptions("jdbc:mysql://localhost/test", "test", "password", getClass().getSimpleName()); + var ctxoptions = new DbContextOptions(dboptions); + var conn = DriverManager.getConnection(dboptions.getUrl(), dboptions.getUser(), dboptions.getPassword()); + var db = new MysqlDatabase(conn, dboptions); + var ctx = new Ctx(db, ctxoptions); + ctx.getDatabase().migrate(); + var result = ctx.getCompanies().filter(e -> e.getName().equals("foobar")).toString(); + System.out.println(result); + } + + private void clearMigrationFolders() { + delRecursive(new File(GENERATE_MIGRATIONS_FOLDER_SRC + "MysqlMigrationTest")); + delRecursive(new File(GENERATE_MIGRATIONS_FOLDER_TARGET + "MysqlMigrationTest")); + } + + private void delRecursive(File f) { + if (f.isDirectory()) { + Arrays.stream(Objects.requireNonNull(f.listFiles())) + .filter(e -> !List.of(".", "..").contains(e.getName())) + .forEach(this::delRecursive); + } + f.delete(); + } + + private void generateInitialMigration() { + try { + var javaHome = System.getProperty("java.home"); + var isWindows = System.getProperty("os.name").toLowerCase(Locale.ROOT).equals("win"); + var java = new File(javaHome, "bin/java" + (isWindows ? ".exe" : "")); + var process = new ProcessBuilder() + .command(java.getAbsolutePath(), + "-cp", "target/classes", "jef.main.Main", + "migration", "add", "--cp", "target/test-classes", "-c", "MysqlMigrationTest$Ctx", "-o", GENERATE_MIGRATIONS_FOLDER_SRC + "MysqlMigrationTest", "Initial") + .inheritIO() + .start(); + process.waitFor(); + var exitCode = process.exitValue(); + assertEquals(0, exitCode, "Initial migration generation failed"); + } catch (AssertionError | RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("Error while compiling generated class", t); + } + } + + private void compileInitialMigration() { + try { + var javaHome = System.getProperty("java.home"); + var isWindows = System.getProperty("os.name").toLowerCase(Locale.ROOT).equals("win"); + var javac = new File(javaHome, "bin/javac" + (isWindows ? ".exe" : "")); + var params = new ArrayList<>(List.of( + javac.getAbsolutePath(), + "-cp", "target/classes" + File.pathSeparator + "target/test-classes", + "-encoding", "UTF8", + "-g", //debug symbols + "-d", GENERATE_MIGRATIONS_FOLDER_TARGET //target + )); + Arrays.stream(new File(GENERATE_MIGRATIONS_FOLDER_SRC + "MysqlMigrationTest").listFiles(File::isFile)).map(File::getPath).forEach(params::add); + var process = new ProcessBuilder() + .command(params.toArray(String[]::new)) + .inheritIO() + .start(); + process.waitFor(10, TimeUnit.SECONDS); + var exitCode = process.exitValue(); + assertEquals(0, exitCode, "Initial migration compilation failed"); + } catch (AssertionError | RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("Error while compiling generated class", t); + } + } + + @Getter + public static class Ctx extends DbContext { + @Clazz(Company.class) + private DbSet companies; + @Clazz(Employee.class) + private DbSet employees; + + public Ctx(Database database, DbContextOptions options) { + super(database, options); + } + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Company extends SerializableObject { + private int id; + private String name; + + private Employee ceo; + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Employee extends SerializableObject { + private int id; + private String name; + + private Employee boss; + @ForeignKey(getterOrField = "boss") + private int bossId; + + private Company company; + @ForeignKey(getterOrField = "company") + private int companyId; + } +}