/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.server.querymaster.query.validate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.diqube.context.AutoInstatiate;
import org.diqube.data.column.ColumnType;
import org.diqube.diql.request.ComparisonRequest;
import org.diqube.diql.request.ExecutionRequest;
import org.diqube.diql.request.FunctionRequest;
import org.diqube.metadata.TableMetadataManager;
import org.diqube.metadata.create.FieldUtil;
import org.diqube.metadata.inspect.TableMetadataInspector;
import org.diqube.metadata.inspect.TableMetadataInspectorFactory;
import org.diqube.metadata.inspect.exception.ColumnNameInvalidException;
import org.diqube.name.FlattenedTableNameUtil;
import org.diqube.plan.PlannerColumnInfo;
import org.diqube.plan.exception.ValidationException;
import org.diqube.plan.validate.ExecutionRequestValidator;
import org.diqube.server.querymaster.query.datatype.DataTypeInvalidException;
import org.diqube.server.querymaster.query.datatype.QueryDataTypeResolver;
import org.diqube.server.querymaster.query.datatype.QueryDataTypeResolverFactory;
import org.diqube.thrift.base.thrift.AuthorizationException;
import org.diqube.thrift.base.thrift.FieldMetadata;
import org.diqube.thrift.base.thrift.TableMetadata;
import org.diqube.util.ColumnOrValue;
/**
* Additional validation in diqube-server of queries being built, which takes into account potentially available
* {@link TableMetadata}.
*
* @author Bastian Gloeckle
*/
@AutoInstatiate
public class MasterExecutionRequestValidator implements ExecutionRequestValidator {
@Inject
private FlattenedTableNameUtil flattenTableNameUtil;
@Inject
private TableMetadataManager metadataManager;
@Inject
private TableMetadataInspectorFactory tableMetadataInspectorFactory;
@Inject
private QueryDataTypeResolverFactory queryDataTypeResolverFactory;
@Override
public void validate(ExecutionRequest executionRequest, Map<String, PlannerColumnInfo> colInfos)
throws ValidationException {
if (executionRequest.getFromRequest().isFlattened()) {
// validate things on ORIGINAL table in case we select from a flattened one.
try {
TableMetadata originalMetadata =
metadataManager.getCurrentTableMetadata(executionRequest.getFromRequest().getTable());
if (originalMetadata != null) {
TableMetadataInspector originalInspector = tableMetadataInspectorFactory.createInspector(originalMetadata);
try {
FieldMetadata m =
originalInspector.findFieldMetadata(executionRequest.getFromRequest().getFlattenByField());
if (m != null && !m.isRepeated())
throw new ValidationException("Cannot flatten by '"
+ executionRequest.getFromRequest().getFlattenByField() + "' since that field is not repeated.");
} catch (ColumnNameInvalidException e) {
// flattenBy field invalid on original table!
throw new ValidationException(e.getMessage(), e);
}
}
} catch (AuthorizationException e) {
// swallow. At max, there is no metadata available, in which case we simply do not validate.
}
}
// prepare validating things on the table that is actually selected from.
String finalTableName;
if (executionRequest.getFromRequest().isFlattened())
finalTableName = flattenTableNameUtil.createIncompleteFlattenedTableName(
executionRequest.getFromRequest().getTable(), executionRequest.getFromRequest().getFlattenByField());
else
finalTableName = executionRequest.getFromRequest().getTable();
TableMetadata tableMetadata;
// try to load metadata and validate columnName if metadata is available.
try {
tableMetadata = metadataManager.getCurrentTableMetadata(finalTableName);
} catch (AuthorizationException e) {
tableMetadata = null;
}
if (tableMetadata != null)
// Note that the tableMetadata we loaded might actually differ from the one that the ExecutablePlan (=
// FlattenStep) actually picks up later! That could be in the case the underlying table is changed in between (new
// shards loaded, shards unloaded). But anyway, if we find a metadata here, we validte using that one. If the
// table is really currently changed and the new TabelMetadata would differ from the current /and/ our query will
// be invalid on the new one - then the user can simply re-run the query.
validate(executionRequest, colInfos, tableMetadata);
}
/**
* Validate the {@link ExecutionRequest} based on the laoded {@link TableMetadata} of the table that the request
* selects from.
*
* @throws ValidationException
* If invalid.
*/
private void validate(ExecutionRequest executionRequest, Map<String, PlannerColumnInfo> colInfos,
TableMetadata metadata) throws ValidationException {
TableMetadataInspector inspector = tableMetadataInspectorFactory.createInspector(metadata);
Map<String, FieldMetadata> rootColMetadata = validateRootColumnsAvailable(executionRequest, inspector);
Map<String, FieldMetadata> tempMetadata = validateFunctionRequestDataTypes(executionRequest, inspector);
// all interesting metadata by field name.
Map<String, FieldMetadata> allMetadata = new HashMap<>(rootColMetadata);
allMetadata.putAll(tempMetadata);
validateComparisonDataTypes(executionRequest, allMetadata);
}
/**
* validate information on all the "root columns", i.e. those columns of the table that are actually accessed by the
* query.
*
* @return Map from field name to {@link FieldMetadata} of the root columns.
*/
private Map<String, FieldMetadata> validateRootColumnsAvailable(ExecutionRequest executionRequest,
TableMetadataInspector inspector) {
Map<String, FieldMetadata> rootColMetadata = new HashMap<>();
for (String rootColName : executionRequest.getAdditionalInfo().getColumnNamesRequired()) {
try {
FieldMetadata m = inspector.findFieldMetadata(rootColName);
if (m != null)
rootColMetadata.put(rootColName, m);
} catch (ColumnNameInvalidException e) {
// column name is invalid/does not exist.
throw new ValidationException(e.getMessage(), e);
}
}
return rootColMetadata;
}
/**
* validate the data types of {@link FunctionRequest}s: Check if there are actually valid projection/aggregation
* functions available for these data types.
*
* @return Map from Field Name to {@link FieldMetadata} of those temporary columns/fields produced by the
* {@link FunctionRequest}s.
*/
private Map<String, FieldMetadata> validateFunctionRequestDataTypes(ExecutionRequest executionRequest,
TableMetadataInspector inspector) {
QueryDataTypeResolver dataTypeResolver = queryDataTypeResolverFactory.create(colName -> {
try {
return inspector.findFieldMetadata(colName);
} catch (ColumnNameInvalidException e) {
// swallow as this should actually never happen - we checked the cols before!
return null;
}
});
try {
return dataTypeResolver.calculateDataTypes(executionRequest.getProjectAndAggregate());
} catch (DataTypeInvalidException e) {
// invalid data types/functions not available.
throw new ValidationException(e.getMessage(), e);
}
}
/**
* Validate all comparisons in request (WHERE and HAVING) that left and right side of comparison is of the same data
* type.
*/
private void validateComparisonDataTypes(ExecutionRequest executionRequest, Map<String, FieldMetadata> allMetadata) {
Collection<ComparisonRequest.Leaf> whereLeafs;
if (executionRequest.getWhere() != null)
whereLeafs = executionRequest.getWhere().findRecursivelyAllOfType(ComparisonRequest.Leaf.class);
else
whereLeafs = new ArrayList<>();
Collection<ComparisonRequest.Leaf> havingLeafs;
if (executionRequest.getHaving() != null)
havingLeafs = executionRequest.getHaving().findRecursivelyAllOfType(ComparisonRequest.Leaf.class);
else
havingLeafs = new ArrayList<>();
Set<ComparisonRequest.Leaf> comparisonLeafs = new HashSet<>(whereLeafs);
comparisonLeafs.addAll(havingLeafs);
for (ComparisonRequest.Leaf l : comparisonLeafs) {
ColumnType leftType = findColumnType(allMetadata, l.getLeftColumnName());
ColumnType rightType;
if (l.getRight().getType().equals(ColumnOrValue.Type.COLUMN))
rightType = findColumnType(allMetadata, l.getRight().getColumnName());
else
rightType = toColumnType(l.getRight().getValue());
if (leftType != null && rightType != null) {
// only check if we're sure about both data types
if (!leftType.equals(rightType))
throw new ValidationException(
"Datatypes incompatible (" + leftType + "<->" + rightType + ") on comparison: " + l.toString());
}
}
}
private ColumnType findColumnType(Map<String, FieldMetadata> metadata, String columnName) {
FieldMetadata m = metadata.get(FieldUtil.toFieldName(columnName));
if (m == null && FieldUtil.isLengthColumn(columnName))
return ColumnType.LONG;
if (m != null)
return FieldUtil.toColumnType(m);
return null;
}
private ColumnType toColumnType(Object val) {
if (val instanceof Double)
return ColumnType.DOUBLE;
if (val instanceof Long)
return ColumnType.LONG;
if (val instanceof String)
return ColumnType.STRING;
return null;
}
}