Skip to content

Commit

Permalink
Merge pull request #286 from IGNF/issue_285
Browse files Browse the repository at this point in the history
validator-core: Test ForeignKeyContraint && ForeignKeyValidator
  • Loading branch information
cboucheIGN authored Sep 14, 2022
2 parents 356d7ea + 3d0d38f commit 7e14c6d
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ public List<ForeignKeyMismatch> foreignKeyNotFound(

List<String> conditions = new ArrayList<String>();
for (int i = 0; i < foreignKey.getSourceColumnNames().size(); i++) {
String condition = "a." + foreignKey.getSourceColumnNames().get(i)
String condition = "src." + foreignKey.getSourceColumnNames().get(i)
+ " LIKE "
+ "b." + foreignKey.getTargetColumnNames().get(i);
conditions.add(condition);
+ " target." + foreignKey.getTargetColumnNames().get(i);
if (i == 0) {
conditions.add(" WHERE " + condition);
} else {
conditions.add(" AND " + condition);
}
}

// Query exemple
Expand All @@ -64,15 +68,24 @@ public List<ForeignKeyMismatch> foreignKeyNotFound(
// JOIN PrescriptionUrbaType AS b
// ON (a.TYPEPSC LIKE b.TYPEPSC AND a.STYPEPSC LIKE b.STYPEPSC) )

String query = "SELECT r.__id, r.__file, "
// String query = "SELECT r.__id, r.__file, "
// + String.join(", ", foreignKey.getSourceColumnNames())
// + " FROM " + tableName + " AS r"
// + " WHERE r.__id NOT IN ("
// + " SELECT a.__id "
// + " FROM " + tableName + " AS a"
// + " JOIN " + foreignKey.getTargetTableName() + " AS b"
// + " ON (" + String.join(" AND ", conditions) + ")"
// + " )";

String query = "SELECT src.__id, src.__file, "
+ String.join(", ", foreignKey.getSourceColumnNames())
+ " FROM " + tableName + " AS r"
+ " WHERE r.__id NOT IN ("
+ " SELECT a.__id "
+ " FROM " + tableName + " AS a"
+ " JOIN " + foreignKey.getTargetTableName() + " AS b"
+ " ON (" + String.join(" AND ", conditions) + ")"
+ " )";
+ " FROM " + tableName + " AS src"
+ " WHERE NOT EXISTS ( "
+ " SELECT true "
+ " FROM " + foreignKey.getTargetTableName() + " AS target "
+ String.join("", conditions)
+ ")";
RowIterator it = database.query(query);

List<ForeignKeyMismatch> result = new ArrayList<ForeignKeyMismatch>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fr.ign.validator.model.constraint;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -36,14 +35,6 @@ public class ForeignKeyConstraint {
public ForeignKeyConstraint() {
}

public List<String> getSourceColumnNames() {
return sourceColumnNames;
}

public void setSourceColumnNames(List<String> sourceColumnNames) {
this.sourceColumnNames = sourceColumnNames;
}

public String getTargetTableName() {
return targetTableName;
}
Expand All @@ -52,6 +43,14 @@ public void setTargetTableName(String targetTableName) {
this.targetTableName = targetTableName;
}

public List<String> getSourceColumnNames() {
return sourceColumnNames;
}

public void setSourceColumnNames(List<String> sourceColumnNames) {
this.sourceColumnNames = sourceColumnNames;
}

public List<String> getTargetColumnNames() {
return targetColumnNames;
}
Expand All @@ -72,7 +71,16 @@ public String toString() {

public static ForeignKeyConstraint parseForeignKey(String foreignKeyString) {
// TODO replace regexp to protect from SQL injection
String regex = "\\([a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\\) REFERENCES ([a-zA-Z0-9_]+)\\([a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\\)";
// String regex = "\\([a-zA-Z0-9_]+(, *[a-zA-Z0-9_]+)*\\) +REFERENCES
// +[a-zA-Z0-9_]+ *\\([a-zA-Z0-9_]+(, *[a-zA-Z0-9_]+)*\\)";
// remove whitespaces
foreignKeyString = foreignKeyString.replaceAll("\\(\\s+", "\\(")
.replaceAll("\\)\\s+", "\\)")
.replaceAll(",\\s+", ",")
.replaceAll("\\s+\\(", "\\(")
.replaceAll("\\s+\\)", "\\)")
.replaceAll("\\s+,", ",");
String regex = "\\([a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)*\\)REFERENCES *[a-zA-Z0-9_]+\\([a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)*\\)";
if (!Pattern.matches(regex, foreignKeyString)) {
throw new InvalidModelException(
String.format(
Expand All @@ -81,24 +89,32 @@ public static ForeignKeyConstraint parseForeignKey(String foreignKeyString) {
)
);
}
String[] stringPart = foreignKeyString.split(" REFERENCES ");
String[] sourcePart = stringPart[0].substring(1, stringPart[0].length() - 1).split(",");

String[] stringPart = foreignKeyString.split("REFERENCES");
String[] sourceColumns = stringPart[0].substring(1, stringPart[0].length() - 1).split(",");
String[] targetPart = stringPart[1].split("\\(");
String[] targetColumn = targetPart[1].substring(0, targetPart[1].length() - 1).split(",");
String[] targetColumns = targetPart[1].substring(0, targetPart[1].length() - 1).split(",");

if (sourcePart.length != targetColumn.length) {
if (sourceColumns.length != targetColumns.length) {
throw new InvalidModelException(
String.format(
"ForeignKeyConstraint - unable to parse key %s",
"ForeignKeyConstraint - unable to parse key %s - columns length must match",
foreignKeyString
)
);
}

ForeignKeyConstraint constraint = new ForeignKeyConstraint();
constraint.setSourceColumnNames(Arrays.asList(sourcePart));
constraint.setTargetTableName(targetPart[0]);
constraint.setTargetColumnNames(Arrays.asList(targetColumn));
constraint.setTargetTableName(targetPart[0].trim());

for (String column : sourceColumns) {
constraint.getSourceColumnNames().add(column.trim());
}

for (String column : targetColumns) {
constraint.getTargetColumnNames().add(column.trim());
}

return constraint;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package fr.ign.validator.model.constraint;

import org.junit.Assert;
import org.junit.Test;

import fr.ign.validator.exception.InvalidModelException;

public class ForeignKeyConstraintTest {

@Test
public void testParseOk() {
String constraintString = "(VALUE, SUB_VALUE) REFERENCES MY_REFERENCE(TYPE, SUB_TYPE)";
ForeignKeyConstraint constraint = ForeignKeyConstraint.parseForeignKey(constraintString);

Assert.assertEquals("(VALUE,SUB_VALUE) REFERENCES MY_REFERENCE(TYPE,SUB_TYPE)", constraint.toString());
}

@Test
public void testParseFormat2() {
String constraintString = " ( VALUE , SUB_VALUE ) REFERENCES MY_REFERENCE ( TYPE, SUB_TYPE ) ";
ForeignKeyConstraint constraint = ForeignKeyConstraint.parseForeignKey(constraintString);

Assert.assertEquals("(VALUE,SUB_VALUE) REFERENCES MY_REFERENCE(TYPE,SUB_TYPE)", constraint.toString());
}

@Test(expected = InvalidModelException.class)
public void testParseErrorCommmaExpected() {
String constraintString = "(VALUE SUB_VALUE) REFERENCES MY_REFERENCE(TYPE SUB_TYPE)";
ForeignKeyConstraint.parseForeignKey(constraintString);
}

@Test(expected = InvalidModelException.class)
public void testParseErrorReferenceExpected() {
String constraintString = "(VALUE, SUB_VALUE) RFRCNC MY_REFERENCE(TYPE, SUB_TYPE)";
ForeignKeyConstraint.parseForeignKey(constraintString);
}

@Test(expected = InvalidModelException.class)
public void testParseErrorTableExpected() {
String constraintString = "(VALUE, SUB_VALUE) REFERENCES (TYPE, SUB_TYPE)";
ForeignKeyConstraint.parseForeignKey(constraintString);
}

@Test(expected = InvalidModelException.class)
public void testParseErrorParenthesisExpected() {
String constraintString = "VALUE, SUB_VALUE REFERENCES MY_REFERENCE TYPE, SUB_TYPE";
ForeignKeyConstraint.parseForeignKey(constraintString);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package fr.ign.validator.validation.database;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;

import fr.ign.validator.Context;
import fr.ign.validator.database.Database;
import fr.ign.validator.error.CoreErrorCodes;
import fr.ign.validator.error.ErrorScope;
import fr.ign.validator.error.ValidatorError;
import fr.ign.validator.model.AttributeType;
import fr.ign.validator.model.DocumentModel;
import fr.ign.validator.model.FeatureType;
import fr.ign.validator.model.FeatureTypeConstraints;
import fr.ign.validator.model.FileModel;
import fr.ign.validator.model.constraint.ForeignKeyConstraint;
import fr.ign.validator.model.file.SingleTableModel;
import fr.ign.validator.model.type.StringType;
import fr.ign.validator.report.InMemoryReportBuilder;

public class ForeignKeyValidatorTest {

@Rule
public TemporaryFolder folder = new TemporaryFolder();

private InMemoryReportBuilder reportBuilder = new InMemoryReportBuilder();

private Context context;

@Before
public void setUp() throws NoSuchAuthorityCodeException, FactoryException {
context = new Context();
context.setProjection("EPSG:4326");
context.setReportBuilder(reportBuilder);

// creates a List<FileModel> with both FileModel
List<FileModel> fileModels = new ArrayList<>();

{
// creates attributes "TYPE", "SUB_TYPE"
AttributeType<String> attribute = new StringType();
attribute.setName("TYPE");
AttributeType<String> attribute2 = new StringType();
attribute2.setName("SUB_TYPE");

List<AttributeType<?>> attributes = new ArrayList<>();
attributes.add(attribute);
attributes.add(attribute2);

// creates a FeatureType with both attributes
FeatureType featureType = new FeatureType();
featureType.setAttributes(attributes);

SingleTableModel fileModel = new SingleTableModel();
fileModel.setName("MY_REFERENCE");
fileModel.setFeatureType(featureType);
fileModels.add(fileModel);
}

{
// creates attributes, "ID", "VALUE", "SUB_VALUE"
AttributeType<String> attribute = new StringType();
attribute.setName("ID");
AttributeType<String> attribute2 = new StringType();
attribute2.setName("VALUE");
AttributeType<String> attribute3 = new StringType();
attribute3.setName("SUB_VALUE");

List<AttributeType<?>> attributes = new ArrayList<>();
attributes.add(attribute);
attributes.add(attribute2);
attributes.add(attribute3);

// creates a FeatureType with both attributes
FeatureType featureType = new FeatureType();
featureType.setAttributes(attributes);
String foreignKey = "(VALUE, SUB_VALUE) REFERENCES MY_REFERENCE(TYPE, SUB_TYPE)";
FeatureTypeConstraints constraints = new FeatureTypeConstraints();
constraints.getForeignKeys().add(ForeignKeyConstraint.parseForeignKey(foreignKey));
featureType.setConstraints(constraints);

SingleTableModel fileModel = new SingleTableModel();
fileModel.setName("MY_TABLE");
fileModel.setFeatureType(featureType);
fileModels.add(fileModel);
}

// creates a DocumentModel with the List<FileModel>
DocumentModel documentModel = new DocumentModel();
documentModel.setName("SAMPLE_MODEL");
documentModel.setFileModels(fileModels);
context.beginModel(documentModel);
}

@Test
public void testValid() throws Exception {
// creates an empty database
File path = new File(folder.getRoot(), "document_database.db");
Database database = new Database(path);

// add the table TEST into the database
database.query("CREATE TABLE MY_TABLE(__id TEXT, __file TEXT, id TEXT, value TEXT, sub_value TEXT);");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id1', 'type1', 'sub_type10');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id2', 'type1', 'sub_type15');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id3', 'type2', 'sub_type20');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id4', 'type2', 'sub_type20');");

// add the table RELATION into the database
database.query("CREATE TABLE MY_REFERENCE(__id TEXT, __file TEXT, type TEXT, sub_type TEXT);");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type1', 'sub_type10');");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type1', 'sub_type15');");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type2', 'sub_type20');");

// check that the validator doesn't send any error
ForeignKeyValidator validator = new ForeignKeyValidator();
validator.validate(context, database);

Assert.assertEquals(0, reportBuilder.getErrorsByCode(CoreErrorCodes.TABLE_FOREIGN_KEY_NOT_FOUND).size());
}

@Test
public void testNotValid() throws Exception {
// creates an empty database
File path = new File(folder.getRoot(), "document_database.db");
Database database = new Database(path);

// add the table TEST into the database
database.query("CREATE TABLE MY_TABLE(__id TEXT, __file TEXT, id TEXT, value TEXT, sub_value TEXT);");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id1', 'type1', 'sub_type1');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id2', 'type1', 'sub_type2');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id3', 'type2', 'sub_type1');");
database.query("INSERT INTO MY_TABLE(id, value, sub_value) VALUES ('id4', 'type2', 'sub_type1');");

// add the table RELATION into the database
database.query("CREATE TABLE MY_REFERENCE(__id TEXT, __file TEXT, type TEXT, sub_type TEXT);");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type1', 'sub_type1');");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type1', 'sub_type2');");
database.query("INSERT INTO MY_REFERENCE(type, sub_type) VALUES ('type2', 'sub_type5');");

// check that the validator doesn't send any error
ForeignKeyValidator validator = new ForeignKeyValidator();
validator.validate(context, database);

Assert.assertEquals(2, reportBuilder.getErrorsByCode(CoreErrorCodes.TABLE_FOREIGN_KEY_NOT_FOUND).size());

List<ValidatorError> errors = reportBuilder.getErrorsByCode(CoreErrorCodes.TABLE_FOREIGN_KEY_NOT_FOUND);
int index = 0;
// check first error
{
ValidatorError error = errors.get(index++);
assertEquals("--", error.getAttribute());
assertEquals(null, error.getId());
assertEquals("", error.getFeatureId());
assertEquals("MY_TABLE", error.getFileModel());
assertEquals(ErrorScope.FEATURE, error.getScope());
assertEquals(
"La correspondance (VALUE, SUB_VALUE) = (type2, sub_type1) n'est pas autorisée, car non présente dans la liste de référence MY_REFERENCE.",
error.getMessage()
);
assertEquals("SAMPLE_MODEL", error.getDocumentModel());
}

{
ValidatorError error = errors.get(index++);
assertEquals("--", error.getAttribute());
assertEquals(null, error.getId());
assertEquals("", error.getFeatureId());
assertEquals("MY_TABLE", error.getFileModel());
assertEquals(ErrorScope.FEATURE, error.getScope());
assertEquals(
"La correspondance (VALUE, SUB_VALUE) = (type2, sub_type1) n'est pas autorisée, car non présente dans la liste de référence MY_REFERENCE.",
error.getMessage()
);
assertEquals("SAMPLE_MODEL", error.getDocumentModel());
}

}

}

0 comments on commit 7e14c6d

Please sign in to comment.