blob: c36ba672edf78f538dd71baeda7b24c8d6549ad6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.drill.exec.physical.base;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.drill.common.expression.ErrorCollector;
import org.apache.drill.common.expression.ErrorCollectorImpl;
import org.apache.drill.common.expression.ExpressionStringBuilder;
import org.apache.drill.common.expression.LogicalExpression;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.common.expression.ValueExpressions;
import org.apache.drill.common.types.TypeProtos;
import org.apache.drill.common.types.Types;
import org.apache.drill.exec.compile.sig.ConstantExpressionIdentifier;
import org.apache.drill.exec.exception.MetadataException;
import org.apache.drill.exec.expr.ExpressionTreeMaterializer;
import org.apache.drill.exec.expr.FilterBuilder;
import org.apache.drill.exec.expr.FilterPredicate;
import org.apache.drill.exec.expr.fn.FunctionImplementationRegistry;
import org.apache.drill.exec.expr.fn.FunctionLookupContext;
import org.apache.drill.exec.expr.stat.RowsMatch;
import org.apache.drill.exec.metastore.MetadataProviderManager;
import org.apache.drill.exec.metastore.analyze.FileMetadataInfoCollector;
import org.apache.drill.exec.ops.OptimizerRulesContext;
import org.apache.drill.exec.ops.UdfUtilities;
import org.apache.drill.exec.planner.physical.PlannerSettings;
import org.apache.drill.exec.record.MaterializedField;
import org.apache.drill.exec.record.metadata.ColumnMetadata;
import org.apache.drill.exec.record.metadata.TupleMetadata;
import org.apache.drill.exec.record.metadata.TupleSchema;
import org.apache.drill.exec.server.options.OptionManager;
import org.apache.drill.exec.store.ColumnExplorer;
import org.apache.drill.exec.store.dfs.DrillFileSystem;
import org.apache.drill.exec.store.dfs.FileSelection;
import org.apache.drill.exec.store.parquet.FilterEvaluatorUtils;
import org.apache.drill.exec.store.parquet.ParquetTableMetadataUtils;
import org.apache.drill.exec.util.ImpersonationUtil;
import org.apache.drill.metastore.metadata.BaseMetadata;
import org.apache.drill.metastore.metadata.FileMetadata;
import org.apache.drill.metastore.metadata.LocationProvider;
import org.apache.drill.metastore.metadata.Metadata;
import org.apache.drill.metastore.metadata.MetadataType;
import org.apache.drill.metastore.metadata.NonInterestingColumnsMetadata;
import org.apache.drill.metastore.metadata.PartitionMetadata;
import org.apache.drill.metastore.metadata.SegmentMetadata;
import org.apache.drill.metastore.metadata.TableMetadata;
import org.apache.drill.metastore.metadata.TableMetadataProvider;
import org.apache.drill.metastore.metadata.TableMetadataProviderBuilder;
import org.apache.drill.metastore.statistics.ColumnStatistics;
import org.apache.drill.metastore.statistics.ColumnStatisticsKind;
import org.apache.drill.metastore.statistics.Statistic;
import org.apache.drill.metastore.statistics.TableStatisticsKind;
import org.apache.drill.metastore.util.SchemaPathUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.Path;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.apache.drill.exec.ExecConstants.SKIP_RUNTIME_ROWGROUP_PRUNING_KEY;
/**
* Represents table group scan with metadata usage.
*/
public abstract class AbstractGroupScanWithMetadata<P extends TableMetadataProvider> extends AbstractFileGroupScan {
protected P metadataProvider;
// table metadata info
protected TableMetadata tableMetadata;
// partition metadata info: mixed partition values for all partition keys in the same list
protected List<PartitionMetadata> partitions;
protected Map<Path, SegmentMetadata> segments;
protected NonInterestingColumnsMetadata nonInterestingColumnsMetadata;
protected List<SchemaPath> partitionColumns;
protected LogicalExpression filter;
protected List<SchemaPath> columns;
protected Map<Path, FileMetadata> files;
// set of the files to be handled
protected Set<Path> fileSet;
// whether all files, partitions or row groups of this group scan fully match the filter
protected boolean matchAllMetadata;
protected boolean usedMetastore; // false by default
protected int maxRecords;
protected AbstractGroupScanWithMetadata(String userName, List<SchemaPath> columns, LogicalExpression filter) {
super(userName);
this.columns = columns;
this.filter = filter;
}
protected AbstractGroupScanWithMetadata(AbstractGroupScanWithMetadata<P> that) {
super(that.getUserName());
this.columns = that.columns;
this.filter = that.filter;
this.matchAllMetadata = that.matchAllMetadata;
this.metadataProvider = that.metadataProvider;
this.tableMetadata = that.tableMetadata;
this.partitionColumns = that.partitionColumns;
this.partitions = that.partitions;
this.segments = that.segments;
this.files = that.files;
this.usedMetastore = that.usedMetastore;
this.nonInterestingColumnsMetadata = that.nonInterestingColumnsMetadata;
this.fileSet = that.fileSet == null ? null : new HashSet<>(that.fileSet);
this.maxRecords = that.maxRecords;
}
@JsonProperty("columns")
@Override
public List<SchemaPath> getColumns() {
return columns;
}
@Override
public Collection<Path> getFiles() {
return fileSet;
}
@Override
public boolean hasFiles() {
return true;
}
@JsonIgnore
public int getMaxRecords() { return maxRecords; }
@JsonIgnore
public boolean isMatchAllMetadata() {
return matchAllMetadata;
}
/**
* Return column value count for the specified column.
* If does not contain such column, return 0.
* Is used when applying convert to direct scan rule.
*
* @param column column schema path
* @return column value count
*/
@Override
public long getColumnValueCount(SchemaPath column) {
ColumnStatistics<?> columnStats = getTableMetadata().getColumnStatistics(column);
ColumnStatistics<?> nonInterestingColStats = columnStats == null
? getNonInterestingColumnsMetadata().getColumnStatistics(column) : null;
long tableRowCount;
if (columnStats != null) {
tableRowCount = TableStatisticsKind.ROW_COUNT.getValue(getTableMetadata());
} else if (nonInterestingColStats != null) {
tableRowCount = TableStatisticsKind.ROW_COUNT.getValue(getNonInterestingColumnsMetadata());
columnStats = nonInterestingColStats;
} else if (hasNestedStatsForColumn(column, getTableMetadata())
|| hasNestedStatsForColumn(column, getNonInterestingColumnsMetadata())) {
// When statistics for nested field exists, this is complex column which is present in table.
// But its nested fields statistics can't be used to extract tableRowCount for this column.
// So NO_COLUMN_STATS returned here to avoid problems described in DRILL-7491.
return Statistic.NO_COLUMN_STATS;
} else {
return 0; // returns 0 if the column doesn't exist in the table.
}
Long nulls = ColumnStatisticsKind.NULLS_COUNT.getFrom(columnStats);
if (nulls == null || Statistic.NO_COLUMN_STATS == nulls || Statistic.NO_COLUMN_STATS == tableRowCount) {
return Statistic.NO_COLUMN_STATS;
} else {
return tableRowCount - nulls;
}
}
/**
* For complex columns, stats may be present only for nested fields. For example, a column path is `a`,
* but stats present for `a`.`b`. So before making a decision that column is absent, the case needs
* to be tested.
*
* @param column parent column path
* @param metadata metadata with column statistics
* @return whether stats exists for nested fields
*/
private boolean hasNestedStatsForColumn(SchemaPath column, Metadata metadata) {
return metadata.getColumnsStatistics().keySet().stream()
.anyMatch(path -> path.contains(column));
}
@Override
public String getDigest() {
return toString();
}
@Override
public ScanStats getScanStats() {
int columnCount = columns == null ? 20 : columns.size();
double rowCount = TableStatisticsKind.ROW_COUNT.getValue(getTableMetadata());
ScanStats scanStats = new ScanStats(ScanStats.GroupScanProperty.EXACT_ROW_COUNT, rowCount, 1, rowCount * columnCount);
logger.trace("Drill parquet scan statistics: {}", scanStats);
return scanStats;
}
// filter push down methods block start
@JsonProperty("filter")
@Override
public LogicalExpression getFilter() {
return filter;
}
@Override
public P getMetadataProvider() {
return metadataProvider;
}
public void setFilter(LogicalExpression filter) {
this.filter = filter;
}
/**
* Set the filter - thus enabling runtime rowgroup pruning
* The runtime pruning can be disabled with an option.
* @param filterExpr The filter to be used at runtime to match with rowgroups' footers
* @param optimizerContext The context for the options
*/
public void setFilterForRuntime(LogicalExpression filterExpr, OptimizerRulesContext optimizerContext) {
OptionManager options = optimizerContext.getPlannerSettings().getOptions();
boolean skipRuntimePruning = options.getBoolean(SKIP_RUNTIME_ROWGROUP_PRUNING_KEY); // if option is set to disable runtime pruning
if ( ! skipRuntimePruning ) { setFilter(filterExpr); }
}
/**
* Applies specified filter {@code filterExpr} to current group scan and produces filtering at:
* <ul>
* <li>table level:
* <ul><li>if filter matches all the the data or prunes all the data, sets corresponding value to
* {@link AbstractGroupScanWithMetadata#isMatchAllMetadata()} and returns null</li></ul></li>
* <li>segment level:
* <ul><li>if filter matches all the the data or prunes all the data, sets corresponding value to
* {@link AbstractGroupScanWithMetadata#isMatchAllMetadata()} and returns null</li>
* <li>if segment metadata was pruned, prunes underlying metadata</li></ul></li>
* <li>partition level:
* <ul><li>if filter matches all the the data or prunes all the data, sets corresponding value to
* {@link AbstractGroupScanWithMetadata#isMatchAllMetadata()} and returns null</li>
* <li>if partition metadata was pruned, prunes underlying metadata</li></ul></li>
* <li>file level:
* <ul><li>if filter matches all the the data or prunes all the data, sets corresponding value to
* {@link AbstractGroupScanWithMetadata#isMatchAllMetadata()} and returns null</li></ul></li>
* </ul>
*
* @param filterExpr filter expression to build
* @param udfUtilities udf utilities
* @param functionImplementationRegistry context to find drill function holder
* @param optionManager option manager
* @return group scan with applied filter expression
*/
@Override
public AbstractGroupScanWithMetadata<?> applyFilter(LogicalExpression filterExpr, UdfUtilities udfUtilities,
FunctionImplementationRegistry functionImplementationRegistry, OptionManager optionManager) {
// Builds filter for pruning. If filter cannot be built, null should be returned.
FilterPredicate<?> filterPredicate =
getFilterPredicate(filterExpr, udfUtilities, functionImplementationRegistry, optionManager, true);
if (filterPredicate == null) {
logger.debug("FilterPredicate cannot be built.");
return null;
}
GroupScanWithMetadataFilterer<?> filteredMetadata = getFilterer()
.filterExpression(filterExpr)
.schema(tableMetadata.getSchema())
.context(functionImplementationRegistry)
.udfUtilities(udfUtilities)
.getFiltered(optionManager, filterPredicate);
if (isGroupScanFullyMatchesFilter(filteredMetadata)) {
logger.debug("applyFilter() does not have any pruning since GroupScan fully matches filter");
matchAllMetadata = filteredMetadata.isMatchAllMetadata();
return null;
}
if (isAllDataPruned(filteredMetadata)) {
if (getFilesMetadata().size() == 1) {
// For the case when group scan has single file and it was filtered,
// no need to create new group scan with the same file.
return null;
}
logger.debug("All files have been filtered out. Add back one to get schema from scanner");
Map<Path, FileMetadata> filesMap = getNextOrEmpty(getFilesMetadata().values()).stream()
.collect(Collectors.toMap(FileMetadata::getPath, Function.identity()));
Map<Path, SegmentMetadata> segmentsMap = getNextOrEmpty(getSegmentsMetadata().values()).stream()
.collect(Collectors.toMap(SegmentMetadata::getPath, Function.identity()));
filteredMetadata.table(getTableMetadata())
.segments(segmentsMap)
.partitions(getNextOrEmpty(getPartitionsMetadata()))
.files(filesMap)
.nonInterestingColumns(getNonInterestingColumnsMetadata())
.matching(false);
}
return filteredMetadata.build();
}
protected boolean isAllDataPruned(GroupScanWithMetadataFilterer<?> filteredMetadata) {
return !filteredMetadata.isMatchAllMetadata()
// filter returns empty result using table metadata
&& (filteredMetadata.getTableMetadata() == null && getTableMetadata() != null)
// all partitions are pruned if segment metadata is available
|| filteredMetadata.getSegments().isEmpty() && !getSegmentsMetadata().isEmpty()
// all segments are pruned if partition metadata is available
|| filteredMetadata.getPartitions().isEmpty() && !getPartitionsMetadata().isEmpty()
// all files are pruned if file metadata is available
|| filteredMetadata.getFiles().isEmpty() && !getFilesMetadata().isEmpty();
}
protected boolean isGroupScanFullyMatchesFilter(GroupScanWithMetadataFilterer<?> filteredMetadata) {
if (MapUtils.isNotEmpty(getFilesMetadata())) {
return getFilesMetadata().size() == filteredMetadata.getFiles().size();
} else if (CollectionUtils.isNotEmpty(getPartitionsMetadata())) {
return getPartitionsMetadata().size() == filteredMetadata.getPartitions().size();
} else if (MapUtils.isNotEmpty(getSegmentsMetadata())) {
return getSegmentsMetadata().size() == filteredMetadata.getSegments().size();
} else {
return getTableMetadata() != null;
}
}
/**
* Returns list with the first element of input list or empty list if input one was empty.
*
* @param inputList the source of the first element
* @param <T> type of values in the list
* @return list with the first element of input list
*/
protected <T> List<T> getNextOrEmpty(Collection<T> inputList) {
return CollectionUtils.isNotEmpty(inputList) ? Collections.singletonList(inputList.iterator().next()) : Collections.emptyList();
}
/**
* Returns holder for metadata values which provides API to filter metadata
* and build new group scan instance using filtered metadata.
*/
protected abstract GroupScanWithMetadataFilterer<?> getFilterer();
public FilterPredicate<?> getFilterPredicate(LogicalExpression filterExpr,
UdfUtilities udfUtilities,
FunctionLookupContext functionImplementationRegistry,
OptionManager optionManager,
boolean omitUnsupportedExprs) {
return getFilterPredicate(filterExpr, udfUtilities, functionImplementationRegistry, optionManager,
omitUnsupportedExprs, supportsFileImplicitColumns(), getTableMetadata().getSchema());
}
/**
* Returns parquet filter predicate built from specified {@code filterExpr}.
*
* @param filterExpr filter expression to build
* @param udfUtilities udf utilities
* @param functionImplementationRegistry context to find drill function holder
* @param optionManager option manager
* @param omitUnsupportedExprs whether expressions which cannot be converted
* may be omitted from the resulting expression
* @param supportsFileImplicitColumns whether implicit columns are supported
* @param schema schema
* @return parquet filter predicate
*/
public static FilterPredicate<?> getFilterPredicate(LogicalExpression filterExpr,
UdfUtilities udfUtilities,
FunctionLookupContext functionImplementationRegistry,
OptionManager optionManager,
boolean omitUnsupportedExprs,
boolean supportsFileImplicitColumns,
TupleMetadata schema) {
TupleMetadata types = schema.copy();
Set<SchemaPath> schemaPathsInExpr = filterExpr.accept(FilterEvaluatorUtils.FieldReferenceFinder.INSTANCE, null);
// adds implicit or partition columns if they weren't added before.
if (supportsFileImplicitColumns) {
for (SchemaPath schemaPath : schemaPathsInExpr) {
if (isImplicitOrPartCol(schemaPath, optionManager) && SchemaPathUtils.getColumnMetadata(schemaPath, types) == null) {
types.add(MaterializedField.create(schemaPath.getRootSegmentPath(), Types.required(TypeProtos.MinorType.VARCHAR)));
}
}
}
ErrorCollector errorCollector = new ErrorCollectorImpl();
LogicalExpression materializedFilter = ExpressionTreeMaterializer.materializeFilterExpr(
filterExpr, types, errorCollector, functionImplementationRegistry);
if (errorCollector.hasErrors()) {
logger.error("{} error(s) encountered when materialize filter expression : {}",
errorCollector.getErrorCount(), errorCollector.toErrorString());
return null;
}
if (logger.isDebugEnabled()) {
logger.debug("materializedFilter : {}", ExpressionStringBuilder.toString(materializedFilter));
}
Set<LogicalExpression> constantBoundaries = ConstantExpressionIdentifier.getConstantExpressionSet(materializedFilter);
return FilterBuilder.buildFilterPredicate(materializedFilter, constantBoundaries, udfUtilities, omitUnsupportedExprs);
}
@JsonProperty
public TupleMetadata getSchema() {
// creates a copy of TupleMetadata from tableMetadata
TupleMetadata tuple = new TupleSchema();
for (ColumnMetadata md : getTableMetadata().getSchema()) {
tuple.addColumn(md.copy());
}
return tuple;
}
// limit push down methods start
@Override
public boolean supportsLimitPushdown() {
return true;
}
@Override
public GroupScan applyLimit(int maxRecords) {
maxRecords = Math.max(maxRecords, 1); // Make sure it request at least 1 row -> 1 file.
GroupScanWithMetadataFilterer<?> prunedMetadata = getFilterer();
if (getTableMetadata() != null) {
long tableRowCount = TableStatisticsKind.ROW_COUNT.getValue(getTableMetadata());
if (tableRowCount == Statistic.NO_COLUMN_STATS || tableRowCount <= maxRecords) {
logger.debug("limit push down does not apply, since total number of rows [{}] is less or equal to the required [{}].",
tableRowCount, maxRecords);
// Return the group scan with the limit pushed down
if (this.maxRecords != maxRecords) {
prunedMetadata.limit(maxRecords);
return prunedMetadata.build();
}
return null;
}
}
// Calculate number of files to read based on maxRecords and update
// number of records to read for each of those files.
List<FileMetadata> qualifiedFiles = limitMetadata(getFilesMetadata().values(), maxRecords);
// some files does not have set row count, do not do files pruning
if (qualifiedFiles == null || qualifiedFiles.size() == getFilesMetadata().size()) {
logger.debug("limit push down does not apply, since number of files was not reduced.");
// Return the group scan with the limit pushed down
if (this.maxRecords != maxRecords) {
prunedMetadata.limit(maxRecords);
return prunedMetadata.build();
}
return null;
}
Map<Path, FileMetadata> filesMap = qualifiedFiles.stream()
.collect(Collectors.toMap(FileMetadata::getPath, Function.identity()));
return prunedMetadata
.table(getTableMetadata())
.segments(getSegmentsMetadata())
.partitions(getPartitionsMetadata())
.files(filesMap)
.limit(maxRecords)
.nonInterestingColumns(getNonInterestingColumnsMetadata())
.matching(matchAllMetadata)
.build();
}
/**
* Removes metadata which does not belong to any of partitions in metadata list.
*
* @param metadataToPrune list of metadata which should be pruned
* @param filteredPartitionMetadata list of partition metadata which was pruned
* @param <T> type of metadata to filter
* @return list with metadata which belongs to pruned partitions
*/
protected static <T extends BaseMetadata & LocationProvider> Map<Path, T> pruneForPartitions(Map<Path, T> metadataToPrune, List<PartitionMetadata> filteredPartitionMetadata) {
Map<Path, T> prunedFiles = new LinkedHashMap<>();
if (metadataToPrune != null) {
metadataToPrune.forEach((path, metadata) -> {
for (PartitionMetadata filteredPartition : filteredPartitionMetadata) {
if (filteredPartition.getLocations().contains(path)) {
prunedFiles.put(path, metadata);
break;
}
}
});
}
return prunedFiles;
}
/**
* Prunes specified metadata list and leaves minimum metadata instances count with general rows number
* which is not less than specified {@code maxRecords}.
*
* @param metadataList list of metadata to prune
* @param maxRecords rows number to leave
* @param <T> type of metadata to prune
* @return pruned metadata list
*/
protected <T extends BaseMetadata> List<T> limitMetadata(Collection<T> metadataList, int maxRecords) {
List<T> qualifiedMetadata = new ArrayList<>();
int currentRowCount = 0;
for (T metadata : metadataList) {
long rowCount = TableStatisticsKind.ROW_COUNT.getValue(metadata);
if (rowCount == Statistic.NO_COLUMN_STATS) {
return null;
} else if (currentRowCount + rowCount <= maxRecords) {
currentRowCount += rowCount;
qualifiedMetadata.add(metadata);
continue;
} else if (currentRowCount < maxRecords) {
qualifiedMetadata.add(metadata);
}
break;
}
return qualifiedMetadata;
}
// limit push down methods end
// partition pruning methods start
@Override
public List<SchemaPath> getPartitionColumns() {
if (partitionColumns == null) {
partitionColumns = metadataProvider.getPartitionColumns();
if (partitionColumns == null) {
partitionColumns = new ArrayList<>();
}
}
return partitionColumns;
}
@JsonIgnore
public TypeProtos.MajorType getTypeForColumn(SchemaPath schemaPath) {
ColumnMetadata columnMetadata = SchemaPathUtils.getColumnMetadata(schemaPath, getTableMetadata().getSchema());
return columnMetadata != null ? columnMetadata.majorType() : null;
}
@JsonIgnore
public <T> T getPartitionValue(Path path, SchemaPath column, Class<T> clazz) {
return getPartitionsMetadata().stream()
.filter(partition -> partition.getColumn().equals(column) && partition.getLocations().contains(path))
.findAny()
.map(metadata -> clazz.cast(metadata.getColumnsStatistics().get(column).get(ColumnStatisticsKind.MAX_VALUE)))
.orElse(null);
}
@JsonIgnore
public Set<Path> getFileSet() {
return fileSet;
}
// partition pruning methods end
// helper method used for partition pruning and filter push down
@Override
public void modifyFileSelection(FileSelection selection) {
fileSet = new HashSet<>(selection.getFiles());
}
// protected methods block
protected void init() throws IOException {
if (fileSet == null && getFilesMetadata() != null) {
fileSet = getFilesMetadata().keySet();
}
}
protected String getFilterString() {
return filter == null || filter.equals(ValueExpressions.BooleanExpression.TRUE) ?
"" : ExpressionStringBuilder.toString(this.filter);
}
protected abstract boolean supportsFileImplicitColumns();
protected abstract List<String> getPartitionValues(LocationProvider locationProvider);
public static boolean isImplicitOrPartCol(SchemaPath schemaPath, OptionManager optionManager) {
Set<String> implicitColNames = ColumnExplorer.initImplicitFileColumns(optionManager).keySet();
return ColumnExplorer.isPartitionColumn(optionManager, schemaPath) || implicitColNames.contains(schemaPath.getRootSegmentPath());
}
@JsonIgnore
public Map<Path, FileMetadata> getFilesMetadata() {
if (files == null) {
files = metadataProvider.getFilesMetadataMap();
}
return files;
}
@Override
public TableMetadata getTableMetadata() {
if (tableMetadata == null) {
tableMetadata = metadataProvider.getTableMetadata();
}
return tableMetadata;
}
@JsonIgnore
public List<PartitionMetadata> getPartitionsMetadata() {
if (partitions == null) {
partitions = metadataProvider.getPartitionsMetadata();
}
return partitions;
}
@JsonIgnore
public Map<Path, SegmentMetadata> getSegmentsMetadata() {
if (segments == null) {
segments = metadataProvider.getSegmentsMetadataMap();
}
return segments;
}
@Override
public boolean usedMetastore() {
return usedMetastore;
}
@JsonIgnore
public NonInterestingColumnsMetadata getNonInterestingColumnsMetadata() {
if (nonInterestingColumnsMetadata == null) {
nonInterestingColumnsMetadata = metadataProvider.getNonInterestingColumnsMetadata();
}
return nonInterestingColumnsMetadata;
}
/**
* Returns {@link TableMetadataProviderBuilder} instance based on specified
* {@link MetadataProviderManager} source.
*
* @param source metadata provider manager
* @return {@link TableMetadataProviderBuilder} instance
*/
protected abstract TableMetadataProviderBuilder tableMetadataProviderBuilder(MetadataProviderManager source);
/**
* Returns {@link TableMetadataProviderBuilder} instance which may provide metadata
* without using Drill Metastore.
*
* @param source metadata provider manager
* @return {@link TableMetadataProviderBuilder} instance
*/
protected abstract TableMetadataProviderBuilder defaultTableMetadataProviderBuilder(MetadataProviderManager source);
/**
* Compares the last modified time of files obtained from specified selection with
* the Metastore last modified time to determine whether Metastore metadata
* is up-to-date. If metadata is outdated, {@link MetadataException} will be thrown.
*
* @param selection the source of files to check
* @throws MetadataException if metadata is outdated
*/
protected void checkMetadataConsistency(FileSelection selection, Configuration fsConf) throws IOException {
if (metadataProvider.checkMetadataVersion()) {
DrillFileSystem fileSystem =
ImpersonationUtil.createFileSystem(ImpersonationUtil.resolveUserName(getUserName()), fsConf);
List<FileStatus> fileStatuses = FileMetadataInfoCollector.getFileStatuses(selection, fileSystem);
long lastModifiedTime = metadataProvider.getTableMetadata().getLastModifiedTime();
Set<Path> removedFiles = new HashSet<>(metadataProvider.getFilesMetadataMap().keySet());
Set<Path> newFiles = new HashSet<>();
boolean isChanged = false;
for (FileStatus fileStatus : fileStatuses) {
if (!removedFiles.remove(Path.getPathWithoutSchemeAndAuthority(fileStatus.getPath()))) {
newFiles.add(fileStatus.getPath());
}
if (fileStatus.getModificationTime() > lastModifiedTime) {
isChanged = true;
break;
}
}
if (isChanged || !removedFiles.isEmpty() || !newFiles.isEmpty()) {
throw MetadataException.of(MetadataException.MetadataExceptionType.OUTDATED_METADATA);
}
}
}
/**
* This class is responsible for filtering different metadata levels.
*/
protected abstract static class GroupScanWithMetadataFilterer<B extends GroupScanWithMetadataFilterer<B>> {
protected final AbstractGroupScanWithMetadata<? extends TableMetadataProvider> source;
protected boolean matchAllMetadata;
protected TableMetadata tableMetadata;
protected List<PartitionMetadata> partitions = Collections.emptyList();
protected Map<Path, SegmentMetadata> segments = Collections.emptyMap();
protected Map<Path, FileMetadata> files = Collections.emptyMap();
protected NonInterestingColumnsMetadata nonInterestingColumnsMetadata;
// required for rebuilding filter expression for the case of schema change
protected LogicalExpression filterExpression;
protected TupleMetadata tableSchema;
protected UdfUtilities udfUtilities;
protected FunctionLookupContext context;
protected int maxRecords;
// for the case when filtering is possible for partitions, but files count exceeds
// PARQUET_ROWGROUP_FILTER_PUSHDOWN_PLANNING_THRESHOLD, new group scan with at least filtered partitions
// and files which belongs to that partitions may be returned
protected MetadataType overflowLevel = MetadataType.NONE;
public GroupScanWithMetadataFilterer(AbstractGroupScanWithMetadata<?> source) {
this.source = source;
}
/**
* Constructs required implementation of {@link AbstractGroupScanWithMetadata} with filtered metadata.
*
* @return implementation of {@link AbstractGroupScanWithMetadata} with filtered metadata
*/
public abstract AbstractGroupScanWithMetadata<?> build();
public B table(TableMetadata tableMetadata) {
this.tableMetadata = tableMetadata;
return self();
}
public B partitions(List<PartitionMetadata> partitions) {
this.partitions = partitions;
return self();
}
public B segments(Map<Path, SegmentMetadata> segments) {
this.segments = segments;
return self();
}
public B nonInterestingColumns(NonInterestingColumnsMetadata nonInterestingColumns) {
this.nonInterestingColumnsMetadata = nonInterestingColumns;
return self();
}
public B files(Map<Path, FileMetadata> files) {
this.files = files;
return self();
}
public B limit(int maxRecords) {
this.maxRecords = maxRecords;
return self();
}
public B matching(boolean matchAllMetadata) {
this.matchAllMetadata = matchAllMetadata;
return self();
}
public B overflow(MetadataType overflowLevel) {
this.overflowLevel = overflowLevel;
return self();
}
public B filterExpression(LogicalExpression filterExpression) {
this.filterExpression = filterExpression;
return self();
}
public B schema(TupleMetadata tableSchema) {
this.tableSchema = tableSchema;
return self();
}
public B udfUtilities(UdfUtilities udfUtilities) {
this.udfUtilities = udfUtilities;
return self();
}
public B context(FunctionLookupContext context) {
this.context = context;
return self();
}
public boolean isMatchAllMetadata() {
return matchAllMetadata;
}
public TableMetadata getTableMetadata() {
return tableMetadata;
}
public List<PartitionMetadata> getPartitions() {
return partitions;
}
public Map<Path, SegmentMetadata> getSegments() {
return segments;
}
public Map<Path, FileMetadata> getFiles() {
return files;
}
public MetadataType getOverflowLevel() {
return overflowLevel;
}
/**
* Produces filtering of metadata and returns {@link GroupScanWithMetadataFilterer}
* to construct resulting group scan.
*
* @param optionManager option manager
* @param filterPredicate filter expression
* @return this instance with filtered metadata
*/
protected B getFiltered(OptionManager optionManager,
FilterPredicate<?> filterPredicate) {
Objects.requireNonNull(filterExpression, "filterExpression was not set");
Objects.requireNonNull(tableSchema, "tableSchema was not set");
Objects.requireNonNull(udfUtilities, "udfUtilities were not set");
Objects.requireNonNull(context, "context was not set");
Set<SchemaPath> schemaPathsInExpr =
filterExpression.accept(FilterEvaluatorUtils.FieldReferenceFinder.INSTANCE, null);
if (source.getTableMetadata() != null) {
filterTableMetadata(filterPredicate, schemaPathsInExpr);
}
if (source.getSegmentsMetadata() != null) {
filterSegmentMetadata(optionManager, filterPredicate, schemaPathsInExpr);
}
if (source.getPartitionsMetadata() != null) {
filterPartitionMetadata(optionManager, filterPredicate, schemaPathsInExpr);
}
if (source.getFilesMetadata() != null) {
filterFileMetadata(optionManager, filterPredicate, schemaPathsInExpr);
}
return self();
}
/**
* Produces filtering of metadata at table level.
*
* @param filterPredicate filter expression
* @param schemaPathsInExpr columns used in filter expression
*/
protected void filterTableMetadata(FilterPredicate<?> filterPredicate, Set<SchemaPath> schemaPathsInExpr) {
// Filters table metadata. If resulting list is empty, should be used single minimum entity of metadata.
// If table matches fully, nothing is pruned and pruning of underlying metadata is stopped.
matchAllMetadata = true;
List<TableMetadata> filteredTableMetadata = filterAndGetMetadata(schemaPathsInExpr,
Collections.singletonList(source.getTableMetadata()), filterPredicate, null);
if (CollectionUtils.isNotEmpty(filteredTableMetadata)) {
this.tableMetadata = filteredTableMetadata.get(0);
}
}
/**
* Produces filtering of metadata at segment level.
*
* @param optionManager option manager
* @param filterPredicate filter expression
* @param schemaPathsInExpr columns used in filter expression
*/
protected void filterSegmentMetadata(OptionManager optionManager,
FilterPredicate<?> filterPredicate,
Set<SchemaPath> schemaPathsInExpr) {
if (!matchAllMetadata) {
if (!source.getSegmentsMetadata().isEmpty()) {
if (source.getSegmentsMetadata().size() <= optionManager.getOption(
PlannerSettings.PARQUET_ROWGROUP_FILTER_PUSHDOWN_PLANNING_THRESHOLD)) {
matchAllMetadata = true;
segments = filterAndGetMetadata(schemaPathsInExpr,
source.getSegmentsMetadata().values(),
filterPredicate,
optionManager).stream()
.collect(Collectors.toMap(
SegmentMetadata::getPath,
Function.identity(),
(first, second) -> second));
} else {
overflowLevel = MetadataType.SEGMENT;
}
}
} else {
segments = source.getSegmentsMetadata();
}
}
/**
* Produces filtering of metadata at partition level.
*
* @param optionManager option manager
* @param filterPredicate filter expression
* @param schemaPathsInExpr columns used in filter expression
*/
protected void filterPartitionMetadata(OptionManager optionManager,
FilterPredicate<?> filterPredicate,
Set<SchemaPath> schemaPathsInExpr) {
List<PartitionMetadata> prunedPartitions;
if (!source.getSegmentsMetadata().isEmpty()
&& source.getSegmentsMetadata().size() > getSegments().size()) {
// prunes row groups to leave only row groups which are contained by pruned segments
prunedPartitions = pruneForSegments(source.getPartitionsMetadata(), getSegments());
} else {
prunedPartitions = source.getPartitionsMetadata();
}
if (isMatchAllMetadata()) {
partitions = prunedPartitions;
return;
}
if (!source.getPartitionsMetadata().isEmpty()) {
if (source.getPartitionsMetadata().size() <= optionManager.getOption(
PlannerSettings.PARQUET_ROWGROUP_FILTER_PUSHDOWN_PLANNING_THRESHOLD)) {
matchAllMetadata = true;
partitions = filterAndGetMetadata(schemaPathsInExpr, prunedPartitions, filterPredicate, optionManager);
} else {
overflowLevel = MetadataType.PARTITION;
}
}
}
/**
* Produces filtering of metadata at file level.
*
* @param optionManager option manager
* @param filterPredicate filter expression
* @param schemaPathsInExpr columns used in filter expression
*/
protected void filterFileMetadata(OptionManager optionManager,
FilterPredicate<?> filterPredicate,
Set<SchemaPath> schemaPathsInExpr) {
Map<Path, FileMetadata> prunedFiles;
if (!source.getPartitionsMetadata().isEmpty()
&& source.getPartitionsMetadata().size() > getPartitions().size()) {
// prunes files to leave only files which are contained by pruned partitions
prunedFiles = pruneForPartitions(source.getFilesMetadata(), getPartitions());
} else if (!source.getSegmentsMetadata().isEmpty()
&& source.getSegmentsMetadata().size() > getSegments().size()) {
// prunes row groups to leave only row groups which are contained by pruned segments
prunedFiles = pruneForSegments(source.getFilesMetadata(), getSegments());
} else {
prunedFiles = source.getFilesMetadata();
}
if (isMatchAllMetadata()) {
files = prunedFiles;
return;
}
// Stop files pruning for the case:
// - # of files is beyond PARQUET_ROWGROUP_FILTER_PUSHDOWN_PLANNING_THRESHOLD.
if (prunedFiles.size() <= optionManager.getOption(
PlannerSettings.PARQUET_ROWGROUP_FILTER_PUSHDOWN_PLANNING_THRESHOLD)) {
matchAllMetadata = true;
files = filterAndGetMetadata(schemaPathsInExpr, prunedFiles.values(), filterPredicate, optionManager).stream()
.collect(Collectors.toMap(FileMetadata::getPath, Function.identity()));
} else {
matchAllMetadata = false;
files = prunedFiles;
overflowLevel = MetadataType.FILE;
}
}
/**
* Removes metadata which does not belong to any of partitions in metadata list.
*
* @param metadataToPrune list of metadata which should be pruned
* @param filteredSegmentMetadata list of segment metadata which was pruned
* @param <T> type of metadata to filter
* @return map with metadata which belongs to pruned partitions
*/
protected static <T extends BaseMetadata & LocationProvider> Map<Path, T> pruneForSegments(
Map<Path, T> metadataToPrune, Map<Path, SegmentMetadata> filteredSegmentMetadata) {
Map<Path, T> prunedFiles = new HashMap<>();
if (metadataToPrune != null) {
for (Map.Entry<Path, T> entry : metadataToPrune.entrySet()) {
for (SegmentMetadata filteredPartition : filteredSegmentMetadata.values()) {
if (filteredPartition.getLocations().contains(entry.getKey())) {
prunedFiles.put(entry.getKey(), entry.getValue());
break;
}
}
}
}
return prunedFiles;
}
/**
* Removes metadata which does not belong to any of partitions in metadata list.
*
* @param metadataToPrune list of partition metadata which should be pruned
* @param filteredSegmentMetadata list of segment metadata which was pruned
* @return list with metadata which belongs to pruned partitions
*/
protected List<PartitionMetadata> pruneForSegments(
List<PartitionMetadata> metadataToPrune, Map<Path, SegmentMetadata> filteredSegmentMetadata) {
List<PartitionMetadata> prunedPartitions = new ArrayList<>();
if (metadataToPrune != null) {
for (PartitionMetadata partition : metadataToPrune) {
for (SegmentMetadata segment : filteredSegmentMetadata.values()) {
if (!Collections.disjoint(segment.getLocations(), partition.getLocations())) {
prunedPartitions.add(partition);
break;
}
}
}
}
return prunedPartitions;
}
/**
* Produces filtering of specified metadata using specified filter expression and returns filtered metadata.
*
* @param schemaPathsInExpr columns used in filter expression
* @param metadataList metadata to filter
* @param filterPredicate filter expression
* @param optionManager option manager
* @param <T> type of metadata to filter
* @return filtered metadata
*/
public <T extends Metadata> List<T> filterAndGetMetadata(
Set<SchemaPath> schemaPathsInExpr,
Iterable<T> metadataList,
FilterPredicate<?> filterPredicate,
OptionManager optionManager) {
List<T> qualifiedMetadata = new ArrayList<>();
for (T metadata : metadataList) {
TupleMetadata schema = metadata.getSchema();
if (schema != null && !tableSchema.isEquivalent(schema)) {
filterPredicate = getFilterPredicate(filterExpression, udfUtilities,
context, optionManager, true, true, schema);
}
Map<SchemaPath, ColumnStatistics<?>> columnsStatistics = metadata.getColumnsStatistics();
// adds partition (dir) column statistics if it may be used during filter evaluation
if (metadata instanceof LocationProvider && optionManager != null) {
LocationProvider locationProvider = (LocationProvider) metadata;
columnsStatistics = ParquetTableMetadataUtils.addImplicitColumnsStatistics(columnsStatistics,
source.columns, source.getPartitionValues(locationProvider), optionManager,
locationProvider.getPath(), source.supportsFileImplicitColumns());
}
if (source.getNonInterestingColumnsMetadata() != null) {
columnsStatistics.putAll(source.getNonInterestingColumnsMetadata().getColumnsStatistics());
}
RowsMatch match = FilterEvaluatorUtils.matches(filterPredicate,
columnsStatistics, TableStatisticsKind.ROW_COUNT.getValue(metadata),
schema, schemaPathsInExpr);
if (match == RowsMatch.NONE) {
continue; // No file comply to the filter => drop the file
}
if (matchAllMetadata) {
matchAllMetadata = match == RowsMatch.ALL;
}
qualifiedMetadata.add(metadata);
}
if (qualifiedMetadata.isEmpty()) {
matchAllMetadata = false;
}
return qualifiedMetadata;
}
protected abstract B self();
}
}