/* * 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.phoenix.filter; import java.io.DataInput; import java.io.DataOutput; import java.io.EOFException; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.TreeSet; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.expression.Expression; import org.apache.phoenix.expression.KeyValueColumnExpression; import org.apache.phoenix.expression.visitor.ExpressionVisitor; import org.apache.phoenix.expression.visitor.StatelessTraverseAllExpressionVisitor; import org.apache.phoenix.schema.tuple.BaseTuple; import org.apache.phoenix.util.ByteUtil; import org.apache.phoenix.util.ServerUtil; /** * * Modeled after {@link org.apache.hadoop.hbase.filter.SingleColumnValueFilter}, * but for general expression evaluation in the case where multiple KeyValue * columns are referenced in the expression. * */ public abstract class MultiKeyValueComparisonFilter extends BooleanExpressionFilter { private static final byte[] UNITIALIZED_KEY_BUFFER = new byte[0]; private Boolean matchedColumn; protected final IncrementalResultTuple inputTuple = new IncrementalResultTuple(); protected TreeSet<byte[]> cfSet; private byte[] essentialCF = ByteUtil.EMPTY_BYTE_ARRAY; private boolean allCFs; public MultiKeyValueComparisonFilter() { } public MultiKeyValueComparisonFilter(Expression expression, boolean allCFs, byte[] essentialCF) { super(expression); this.allCFs = allCFs; this.essentialCF = essentialCF == null ? ByteUtil.EMPTY_BYTE_ARRAY : essentialCF; init(); } private static final class CellRef { public Cell cell; @Override public String toString() { if(cell != null) { return cell.toString() + " value = " + Bytes.toStringBinary( cell.getValueArray(), cell.getValueOffset(), cell.getValueLength()); } else { return super.toString(); } } } protected abstract Object setColumnKey(byte[] cf, int cfOffset, int cfLength, byte[] cq, int cqOffset, int cqLength); protected abstract Object newColumnKey(byte[] cf, int cfOffset, int cfLength, byte[] cq, int cqOffset, int cqLength); private final class IncrementalResultTuple extends BaseTuple { private int refCount; private final ImmutableBytesWritable keyPtr = new ImmutableBytesWritable(UNITIALIZED_KEY_BUFFER); private final Map<Object,CellRef> foundColumns = new HashMap<Object,CellRef>(5); public void reset() { refCount = 0; keyPtr.set(UNITIALIZED_KEY_BUFFER); for (CellRef ref : foundColumns.values()) { ref.cell = null; } } @Override public boolean isImmutable() { return refCount == foundColumns.size(); } public void setImmutable() { refCount = foundColumns.size(); } private ReturnCode resolveColumn(Cell value) { // Always set key, in case we never find a key value column of interest, // and our expression uses row key columns. setKey(value); Object ptr = setColumnKey(value.getFamilyArray(), value.getFamilyOffset(), value.getFamilyLength(), value.getQualifierArray(), value.getQualifierOffset(), value.getQualifierLength()); CellRef ref = foundColumns.get(ptr); if (ref == null) { // Return INCLUDE_AND_NEXT_COL here. Although this filter doesn't need this KV // it should still be projected into the Result return ReturnCode.INCLUDE_AND_NEXT_COL; } // Since we only look at the latest key value for a given column, // we are not interested in older versions // TODO: test with older versions to confirm this doesn't get tripped // This shouldn't be necessary, because a scan only looks at the latest // version if (ref.cell != null) { // Can't do NEXT_ROW, because then we don't match the other columns // SKIP, INCLUDE, and NEXT_COL seem to all act the same return ReturnCode.NEXT_COL; } ref.cell = value; refCount++; return null; } public void addColumn(byte[] cf, byte[] cq) { Object ptr = MultiKeyValueComparisonFilter.this.newColumnKey(cf, 0, cf.length, cq, 0, cq.length); foundColumns.put(ptr, new CellRef()); } public void setKey(Cell value) { keyPtr.set(value.getRowArray(), value.getRowOffset(), value.getRowLength()); } @Override public void getKey(ImmutableBytesWritable ptr) { ptr.set(keyPtr.get(),keyPtr.getOffset(),keyPtr.getLength()); } @Override public Cell getValue(byte[] cf, byte[] cq) { Object ptr = setColumnKey(cf, 0, cf.length, cq, 0, cq.length); CellRef ref = foundColumns.get(ptr); return ref == null ? null : ref.cell; } @Override public String toString() { return foundColumns.toString(); } @Override public int size() { return refCount; } @Override public Cell getValue(int index) { // This won't perform very well, but it's not // currently used anyway for (CellRef ref : foundColumns.values()) { if (ref.cell == null) { continue; } if (index == 0) { return ref.cell; } index--; } throw new IndexOutOfBoundsException(Integer.toString(index)); } @Override public boolean getValue(byte[] family, byte[] qualifier, ImmutableBytesWritable ptr) { Cell cell = getValue(family, qualifier); if (cell == null) return false; ptr.set(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength()); return true; } } protected void init() { cfSet = new TreeSet<byte[]>(Bytes.BYTES_COMPARATOR); ExpressionVisitor<Void> visitor = new StatelessTraverseAllExpressionVisitor<Void>() { @Override public Void visit(KeyValueColumnExpression expression) { inputTuple.addColumn(expression.getColumnFamily(), expression.getColumnQualifier()); return null; } }; expression.accept(visitor); } @Override public ReturnCode filterKeyValue(Cell cell) { if (Boolean.TRUE.equals(this.matchedColumn)) { // We already found and matched the single column, all keys now pass return ReturnCode.INCLUDE_AND_NEXT_COL; } if (Boolean.FALSE.equals(this.matchedColumn)) { // We found all the columns, but did not match the expression, so skip to next row return ReturnCode.NEXT_ROW; } // This is a key value we're not interested in (TODO: why INCLUDE here instead of NEXT_COL?) ReturnCode code = inputTuple.resolveColumn(cell); if (code != null) { return code; } // We found a new column, so we can re-evaluate // TODO: if we have row key columns in our expression, should // we always evaluate or just wait until the end? this.matchedColumn = this.evaluate(inputTuple); if (this.matchedColumn == null) { if (inputTuple.isImmutable()) { this.matchedColumn = Boolean.FALSE; } else { return ReturnCode.INCLUDE_AND_NEXT_COL; } } return this.matchedColumn ? ReturnCode.INCLUDE_AND_NEXT_COL : ReturnCode.NEXT_ROW; } @Override public boolean filterRow() { if (this.matchedColumn == null && !inputTuple.isImmutable() && expression.requiresFinalEvaluation()) { inputTuple.setImmutable(); this.matchedColumn = this.evaluate(inputTuple); } return ! (Boolean.TRUE.equals(this.matchedColumn)); } @Override public void reset() { matchedColumn = null; inputTuple.reset(); super.reset(); } @Override public boolean isFamilyEssential(byte[] name) { // Typically only the column families involved in the expression are essential. // The others are for columns projected in the select expression. However, depending // on the expression (i.e. IS NULL), we may need to include the column family // containing the empty key value or all column families in the case of a mapped // view (where we don't have an empty key value). return allCFs || Bytes.compareTo(name, essentialCF) == 0 || cfSet.contains(name); } @Override public void readFields(DataInput input) throws IOException { super.readFields(input); try { allCFs = input.readBoolean(); if (!allCFs) { essentialCF = Bytes.readByteArray(input); } } catch (EOFException e) { // Ignore as this will occur when a 4.10 client is used } init(); } @Override public void write(DataOutput output) throws IOException { super.write(output); try { output.writeBoolean(allCFs); if (!allCFs) { Bytes.writeByteArray(output, essentialCF); } } catch (Throwable t) { // Catches incompatibilities during reading/writing and doesn't retry ServerUtil.throwIOException("MultiKeyValueComparisonFilter failed during writing", t); } } }