diff --git a/README.md b/README.md index 2edb129..d306e7a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Todo ## Next steps -- onvalidate function, check illegal chars in names, name collitions - use builders for entities/fields/keys - make work for mariadb/mysql - add database object to context with diff --git a/src/main/java/jef/model/DbContext.java b/src/main/java/jef/model/DbContext.java index 582a26f..fa31dc6 100644 --- a/src/main/java/jef/model/DbContext.java +++ b/src/main/java/jef/model/DbContext.java @@ -1,6 +1,82 @@ package jef.model; +import jef.model.constraints.ForeignKeyConstraint; +import jef.serializable.SerializableObject; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; + public abstract class DbContext { + private static final String ILLEGAL_CHARACTERS = "\"'`,.";//TODO also validate this in modelbuilder/dbentity/dbfield + public void onModelCreate(ModelBuilder mb) { } + + public void onModelValidate(ModelBuilder mb) { + var errors = new ArrayList(); + + //check for illegal characters + for (var entity : mb.getEntities()) { + if (entity.getName().matches("[" + ILLEGAL_CHARACTERS + "]")) { + errors.add("DbEntity " + entity.getTypeName() + " has illegal characters in its name: " + entity.getName()); + } + for (var field : entity.getFields()) { + if (field.getName().matches("[" + ILLEGAL_CHARACTERS + "]")) { + var fieldname = Optional.ofNullable(field.getField()).map(Field::getName).orElse(field.getName()); + errors.add("DbField " + entity.getTypeName() + "::" + fieldname + " has illegal characters in its name: " + entity.getName()); + } + } + } + + //check for duplicate entity names + var entityNameMap = new HashMap>(); + for (DbEntity entity : mb.getEntities()) { + var name = entity.getName().toLowerCase(); + if (entityNameMap.containsKey(name)) { + errors.add("Duplicate DbEntity with name " + entity.getName() + ": " + entity.getTypeName() + " and " + entityNameMap.get(name).getTypeName()); + } else { + entityNameMap.put(name, entity); + } + } + + //check for duplicate field names + for (DbEntity entity : mb.getEntities()) { + var fieldNameMap = new HashMap>(); + for (DbField field : entity.getFields()) { + var name = field.getName().toLowerCase(); + if (fieldNameMap.containsKey(name)) { + errors.add("Duplicate DbField in entity " + entity.getTypeName() + " with name " + field.getName() + ": " + field.getName() + " and " + fieldNameMap.get(name).getName()); + } else { + fieldNameMap.put(name, entity); + } + } + } + + //check foreign keys for existing primary keys + for (DbEntity entity : mb.getEntities()) { + for (ForeignKeyConstraint foreignKey : entity.getForeignKeys()) { + //verify primary key exists + if (foreignKey.getReferencedEntity().getPrimaryKey() == null) { + errors.add("DbEntity " + entity.getTypeName() + " has a foreign key constraint to " + foreignKey.getReferencedEntity().getTypeName() + + " which does not have primary key"); + } + //verify field count, names, types, and order + else if (!foreignKey.getReferencedEntity().getPrimaryKey().getFields().equals(foreignKey.getReferencedFields())) { + errors.add("DbEntity " + entity.getTypeName() + " has a foreign key constraint to " + foreignKey.getReferencedEntity().getTypeName() + + " where the specified fields do not match: referring fields [" + + foreignKey.getReferencedFields().stream().map(DbField::getName).collect(Collectors.joining(", ")) + + "] but found referenced fields [" + + foreignKey.getReferencedEntity().getPrimaryKey().getFields().stream().map(DbField::getName).collect(Collectors.joining(", ")) + + "]"); + } + } + } + + if (!errors.isEmpty()) { + throw new ModelException(String.join("\n", errors)); + } + } } diff --git a/src/main/java/jef/model/DbEntity.java b/src/main/java/jef/model/DbEntity.java index 3976f10..187d9ff 100644 --- a/src/main/java/jef/model/DbEntity.java +++ b/src/main/java/jef/model/DbEntity.java @@ -179,17 +179,12 @@ public class DbEntity { return this.indexes.remove(index); } - private String extractFieldName(SerializableFunction getter) { - try { - var expr = new AsmParser(getter).parse(); - if (expr.getType() != Expression.Type.INTERMEDIATE_FIELD) { - throw new RuntimeException(expr.getClass().getSimpleName() + " is not a field expression"); - } - var name = ((IntermediateFieldExpression) expr).getName(); - return name; - } catch (AsmParseException e) { - throw new RuntimeException("Invalid expression", e); - } + /** + * Does not validate foreign key integrity + * @param primaryKey + */ + public void setPrimaryKey(PrimaryKeyConstraint primaryKey) { + this.primaryKey = primaryKey; } public void addForeignKey(ForeignKeyConstraint foreignKey) { @@ -216,6 +211,19 @@ public class DbEntity { } } + private String extractFieldName(SerializableFunction getter) { + try { + var expr = new AsmParser(getter).parse(); + if (expr.getType() != Expression.Type.INTERMEDIATE_FIELD) { + throw new RuntimeException(expr.getClass().getSimpleName() + " is not a field expression"); + } + var name = ((IntermediateFieldExpression) expr).getName(); + return name; + } catch (AsmParseException e) { + throw new RuntimeException("Invalid expression", e); + } + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/jef/model/ModelBuilder.java b/src/main/java/jef/model/ModelBuilder.java index a35d711..1aec282 100644 --- a/src/main/java/jef/model/ModelBuilder.java +++ b/src/main/java/jef/model/ModelBuilder.java @@ -56,6 +56,7 @@ public class ModelBuilder { var instance = context.getDeclaredConstructor().newInstance();//TODO find constructor, //TODO optionally with config instance.onModelCreate(mb); + instance.onModelValidate(mb); return mb; } diff --git a/src/main/java/jef/model/migration/creator/MigrationCreator.java b/src/main/java/jef/model/migration/creator/MigrationCreator.java index 48c1058..74a1a24 100644 --- a/src/main/java/jef/model/migration/creator/MigrationCreator.java +++ b/src/main/java/jef/model/migration/creator/MigrationCreator.java @@ -33,7 +33,7 @@ import java.util.List; import java.util.stream.Collectors; public class MigrationCreator { - private static final String ILLEGAL_CHARACTERS = "\"'`,.";//TODO also validate this in modelbuilder/dbentity/dbfield + public Result createMigration(ModelBuilder from, ModelBuilder to, String name, String packageName, String currentSnapshotJava) { var result = new Result(); @@ -70,18 +70,6 @@ public class MigrationCreator { } private Result generateMigration(ModelBuilder from, ModelBuilder to, String name, String packageName, Result result) { - //check for illegal characters - for (var entity : to.getEntities()) { - if (entity.getName().matches("[" + ILLEGAL_CHARACTERS + "]")) { - throw new ModelException("Entity " + entity.getType().getSimpleName() + " has illegal characters in its name: " + entity.getName()); - } - for (var field : entity.getFields()) { - if (field.getName().matches("[" + ILLEGAL_CHARACTERS + "]")) { - throw new ModelException("Field " + entity.getType().getSimpleName() + "::" + field.getField().getName() + " has illegal characters in its name: " + entity.getName()); - } - } - } - //reduce model builders to changes var mcd = new ModelChangeDetector(from, to).detect(); var fromReduced = mcd.getFrom();