blob: 9e0f9282a643d3d4aed7161fa3a072cede08e6c2 [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.phoenix.compile;
import static org.apache.phoenix.query.QueryServices.WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB;
import static org.apache.phoenix.query.QueryServicesOptions.DEFAULT_WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.WritableUtils;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.expression.BaseTerminalExpression;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.KeyValueColumnExpression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.ProjectedColumnExpression;
import org.apache.phoenix.expression.SingleCellColumnExpression;
import org.apache.phoenix.expression.function.ArrayIndexFunction;
import org.apache.phoenix.expression.function.JsonQueryFunction;
import org.apache.phoenix.expression.function.JsonValueFunction;
import org.apache.phoenix.expression.visitor.ExpressionVisitor;
import org.apache.phoenix.expression.visitor.ProjectedColumnExpressionVisitor;
import org.apache.phoenix.expression.visitor.ReplaceArrayFunctionExpressionVisitor;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.parse.AliasedNode;
import org.apache.phoenix.parse.BindParseNode;
import org.apache.phoenix.parse.ColumnParseNode;
import org.apache.phoenix.parse.FamilyWildcardParseNode;
import org.apache.phoenix.parse.FunctionParseNode;
import org.apache.phoenix.parse.ParseNode;
import org.apache.phoenix.parse.PhoenixRowTimestampParseNode;
import org.apache.phoenix.parse.SelectStatement;
import org.apache.phoenix.parse.SequenceValueParseNode;
import org.apache.phoenix.parse.TableName;
import org.apache.phoenix.parse.TableWildcardParseNode;
import org.apache.phoenix.parse.WildcardParseNode;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.AmbiguousColumnException;
import org.apache.phoenix.schema.ArgumentTypeMismatchException;
import org.apache.phoenix.schema.ColumnFamilyNotFoundException;
import org.apache.phoenix.schema.ColumnNotFoundException;
import org.apache.phoenix.schema.ColumnRef;
import org.apache.phoenix.schema.IndexUncoveredDataColumnRef;
import org.apache.phoenix.schema.KeyValueSchema;
import org.apache.phoenix.schema.KeyValueSchema.KeyValueSchemaBuilder;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PColumnFamily;
import org.apache.phoenix.schema.PDatum;
import org.apache.phoenix.schema.PName;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTable.ImmutableStorageScheme;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.ProjectedColumn;
import org.apache.phoenix.schema.RowKeySchema;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.ValueBitSet;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PJson;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.IndexUtil;
import org.apache.phoenix.util.SchemaUtil;
import org.apache.phoenix.util.SizedUtil;
import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
/**
*
* Class that iterates through expressions in SELECT clause and adds projected
* columns to scan.
*
*
* @since 0.1
*/
public class ProjectionCompiler {
private static final Expression NULL_EXPRESSION = LiteralExpression.newConstant(null);
private ProjectionCompiler() {
}
private static void projectColumnFamily(PTable table, Scan scan, byte[] family) {
// Will project all colmuns for given CF
scan.addFamily(family);
}
public static RowProjector compile(StatementContext context, SelectStatement statement, GroupBy groupBy) throws SQLException {
boolean wildcardIncludesDynamicCols = context.getConnection().getQueryServices()
.getConfiguration().getBoolean(WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB,
DEFAULT_WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB);
return compile(context, statement, groupBy, Collections.<PColumn>emptyList(),
// Pass null expression because we don't want empty key value to be projected
NULL_EXPRESSION,
wildcardIncludesDynamicCols);
}
private static int getMinPKOffset(PTable table, PName tenantId) {
// In SELECT *, don't include tenant column or index ID column for tenant connection
int posOffset = table.getBucketNum() == null ? 0 : 1;
if (table.isMultiTenant() && tenantId != null) {
posOffset++;
}
if (table.getViewIndexId() != null) {
posOffset++;
}
return posOffset;
}
private static void projectAllTableColumns(StatementContext context, TableRef tableRef, boolean resolveColumn, List<Expression> projectedExpressions, List<ExpressionProjector> projectedColumns, List<? extends PDatum> targetColumns) throws SQLException {
ColumnResolver resolver = context.getResolver();
PTable table = tableRef.getTable();
int projectedOffset = projectedExpressions.size();
int posOffset = table.getBucketNum() == null ? 0 : 1;
int minPKOffset = getMinPKOffset(table, context.getConnection().getTenantId());
for (int i = posOffset, j = posOffset; i < table.getColumns().size(); i++) {
PColumn column = table.getColumns().get(i);
// Skip tenant ID column (which may not be the first column, but is the first PK column)
if (SchemaUtil.isPKColumn(column) && j++ < minPKOffset) {
posOffset++;
continue;
}
ColumnRef ref = new ColumnRef(tableRef,i);
String colName = ref.getColumn().getName().getString();
String tableAlias = tableRef.getTableAlias();
if (resolveColumn) {
try {
if (tableAlias != null) {
ref = resolver.resolveColumn(null, tableAlias, colName);
} else {
String schemaName = table.getSchemaName().getString();
ref = resolver.resolveColumn(schemaName.length() == 0 ? null : schemaName, table.getTableName().getString(), colName);
}
// The freshly revolved column's family better be the same as the original one.
// If not, trigger the disambiguation logic. Also see PTableImpl.getColumnForColumnName(...)
if (column.getFamilyName() != null && !column.getFamilyName().equals(ref.getColumn().getFamilyName())) {
throw new AmbiguousColumnException();
}
} catch (AmbiguousColumnException e) {
if (column.getFamilyName() != null) {
ref = resolver.resolveColumn(tableAlias != null ? tableAlias : table.getTableName().getString(), column.getFamilyName().getString(), colName);
} else {
throw e;
}
}
}
Expression expression = ref.newColumnExpression();
expression = coerceIfNecessary(i-posOffset+projectedOffset, targetColumns, expression);
ImmutableBytesWritable ptr = context.getTempPtr();
if (IndexUtil.getViewConstantValue(column, ptr)) {
expression = LiteralExpression.newConstant(
column.getDataType().toObject(ptr, column.getSortOrder()),
expression.getDataType(),
column.getSortOrder());
}
projectedExpressions.add(expression);
boolean isCaseSensitive = !SchemaUtil.normalizeIdentifier(colName).equals(colName);
projectedColumns.add(new ExpressionProjector(colName, colName, tableRef.getTableAlias() == null ? table.getName().getString() : tableRef.getTableAlias(), expression, isCaseSensitive));
}
}
private static void projectAllIndexColumns(StatementContext context, TableRef tableRef, boolean resolveColumn, List<Expression> projectedExpressions, List<ExpressionProjector> projectedColumns, List<? extends PDatum> targetColumns) throws SQLException {
ColumnResolver resolver = context.getResolver();
PTable index = tableRef.getTable();
int projectedOffset = projectedExpressions.size();
PhoenixConnection conn = context.getConnection();
PName tenantId = conn.getTenantId();
String dataTableName = index.getParentName().getString();
PTable dataTable = conn.getTable(dataTableName);
int tableOffset = dataTable.getBucketNum() == null ? 0 : 1;
int minTablePKOffset = getMinPKOffset(dataTable, tenantId);
int minIndexPKOffset = getMinPKOffset(index, tenantId);
if (!IndexUtil.shouldIndexBeUsedForUncoveredQuery(tableRef)) {
if (index.getColumns().size()-minIndexPKOffset != dataTable.getColumns().size()-minTablePKOffset) {
// We'll end up not using this by the optimizer, so just throw
String schemaNameStr = dataTable.getSchemaName()==null?null:dataTable.getSchemaName().getString();
String tableNameStr = dataTable.getTableName()==null?null:dataTable.getTableName().getString();
throw new ColumnNotFoundException(schemaNameStr, tableNameStr,null, WildcardParseNode.INSTANCE.toString());
}
}
// At this point, the index table is either fully covered, or we are projecting uncovered
// columns
// The easy thing would be to just call projectAllTableColumns on the projected table,
// but its columns are not in the same order as the data column, so we have to map them to
// the data column order
TableRef projectedTableRef =
new TableRef(resolver.getTables().get(0), tableRef.getTableAlias());
for (int i = tableOffset, j = tableOffset; i < dataTable.getColumns().size(); i++) {
PColumn column = dataTable.getColumns().get(i);
// Skip tenant ID column (which may not be the first column, but is the first PK column)
if (SchemaUtil.isPKColumn(column) && j++ < minTablePKOffset) {
tableOffset++;
continue;
}
PColumn dataTableColumn = dataTable.getColumns().get(i);
String indexColName = IndexUtil.getIndexColumnName(dataTableColumn);
PColumn indexColumn = null;
ColumnRef ref = null;
try {
indexColumn = index.getColumnForColumnName(indexColName);
//TODO could should we do this more efficiently than catching the expcetion ?
} catch (ColumnNotFoundException e) {
if (IndexUtil.shouldIndexBeUsedForUncoveredQuery(tableRef)) {
//Projected columns have the same name as in the data table
String familyName =
dataTableColumn.getFamilyName() == null ? null
: dataTableColumn.getFamilyName().getString();
ref =
resolver.resolveColumn(familyName,
tableRef.getTableAlias() == null
? tableRef.getTable().getName().getString()
: tableRef.getTableAlias(),
indexColName);
indexColumn = ref.getColumn();
} else {
throw e;
}
}
ref = new ColumnRef(projectedTableRef, indexColumn.getPosition());
String colName = dataTableColumn.getName().getString();
String tableAlias = tableRef.getTableAlias();
if (resolveColumn) {
try {
if (tableAlias != null) {
ref = resolver.resolveColumn(null, tableAlias, indexColName);
} else {
String schemaName = index.getSchemaName().getString();
ref = resolver.resolveColumn(schemaName.length() == 0 ? null : schemaName, index.getTableName().getString(), indexColName);
}
} catch (AmbiguousColumnException e) {
if (indexColumn.getFamilyName() != null) {
ref = resolver.resolveColumn(tableAlias != null ? tableAlias : index.getTableName().getString(), indexColumn.getFamilyName().getString(), indexColName);
} else {
throw e;
}
}
}
Expression expression = ref.newColumnExpression();
expression = coerceIfNecessary(i-tableOffset+projectedOffset, targetColumns, expression);
// We do not need to check if the column is a viewConstant, because view constants never
// appear as a column in an index
projectedExpressions.add(expression);
boolean isCaseSensitive = !SchemaUtil.normalizeIdentifier(colName).equals(colName);
ExpressionProjector projector = new ExpressionProjector(colName, colName, tableRef.getTableAlias() == null ? dataTable.getName().getString() : tableRef.getTableAlias(), expression, isCaseSensitive);
projectedColumns.add(projector);
}
}
private static void projectTableColumnFamily(StatementContext context, String cfName, TableRef tableRef, boolean resolveColumn, List<Expression> projectedExpressions, List<ExpressionProjector> projectedColumns) throws SQLException {
PTable table = tableRef.getTable();
PColumnFamily pfamily = table.getColumnFamily(cfName);
for (PColumn column : pfamily.getColumns()) {
ColumnRef ref = new ColumnRef(tableRef, column.getPosition());
if (resolveColumn) {
ref = context.getResolver().resolveColumn(table.getTableName().getString(), cfName, column.getName().getString());
}
Expression expression = ref.newColumnExpression();
projectedExpressions.add(expression);
String colName = column.getName().toString();
boolean isCaseSensitive = !SchemaUtil.normalizeIdentifier(colName).equals(colName);
projectedColumns.add(new ExpressionProjector(colName, colName, tableRef.getTableAlias() == null ?
table.getName().getString() : tableRef.getTableAlias(), expression, isCaseSensitive));
}
}
private static void projectIndexColumnFamily(StatementContext context, String cfName, TableRef tableRef, boolean resolveColumn, List<Expression> projectedExpressions, List<ExpressionProjector> projectedColumns) throws SQLException {
ColumnResolver resolver = context.getResolver();
PTable index = tableRef.getTable();
PhoenixConnection conn = context.getConnection();
String dataTableName = index.getParentName().getString();
PTable dataTable = conn.getTable(dataTableName);
PColumnFamily pfamily = dataTable.getColumnFamily(cfName);
TableRef projectedTableRef =
new TableRef(resolver.getTables().get(0), tableRef.getTableAlias());
PTable projectedIndex = projectedTableRef.getTable();
for (PColumn column : pfamily.getColumns()) {
String indexColName = IndexUtil.getIndexColumnName(column);
PColumn indexColumn = null;
ColumnRef ref = null;
String indexColumnFamily = null;
try {
indexColumn = index.getColumnForColumnName(indexColName);
ref = new ColumnRef(projectedTableRef, indexColumn.getPosition());
indexColumnFamily =
indexColumn.getFamilyName() == null ? null
: indexColumn.getFamilyName().getString();
} catch (ColumnNotFoundException e) {
if (IndexUtil.shouldIndexBeUsedForUncoveredQuery(tableRef)) {
try {
//Projected columns have the same name as in the data table
String colName = column.getName().getString();
String familyName =
column.getFamilyName() == null ? null
: column.getFamilyName().getString();
resolver.resolveColumn(familyName,
tableRef.getTableAlias() == null
? tableRef.getTable().getName().getString()
: tableRef.getTableAlias(),
indexColName);
indexColumn = projectedIndex.getColumnForColumnName(colName);
} catch (ColumnFamilyNotFoundException c) {
throw e;
}
} else {
throw e;
}
}
if (resolveColumn) {
ref =
resolver.resolveColumn(index.getTableName().getString(), indexColumnFamily,
indexColName);
}
Expression expression = ref.newColumnExpression();
projectedExpressions.add(expression);
String colName = column.getName().toString();
boolean isCaseSensitive = !SchemaUtil.normalizeIdentifier(colName).equals(colName);
projectedColumns.add(new ExpressionProjector(colName, colName,
tableRef.getTableAlias() == null ? dataTable.getName().getString()
: tableRef.getTableAlias(),
expression, isCaseSensitive));
}
}
private static Expression coerceIfNecessary(int index, List<? extends PDatum> targetColumns, Expression expression) throws SQLException {
if (index < targetColumns.size()) {
PDatum targetColumn = targetColumns.get(index);
if (targetColumn.getDataType() != expression.getDataType()) {
PDataType targetType = targetColumn.getDataType();
// Check if coerce allowed using more relaxed isCastableTo check, since we promote INTEGER to LONG
// during expression evaluation and then convert back to INTEGER on UPSERT SELECT (and we don't have
// (an actual value we can specifically check against).
if (expression.getDataType() != null && !expression.getDataType().isCastableTo(targetType)) {
throw new ArgumentTypeMismatchException(targetType, expression.getDataType(), "column: " + targetColumn);
}
expression = CoerceExpression.create(expression, targetType, targetColumn.getSortOrder(), targetColumn.getMaxLength());
}
}
return expression;
}
/**
* Builds the projection for the scan
* @param context query context kept between compilation of different query clauses
* @param statement the statement being compiled
* @param groupBy compiled GROUP BY clause
* @param targetColumns list of columns, parallel to aliasedNodes, that are being set for an
* UPSERT SELECT statement. Used to coerce expression types to the expected target type.
* @param where the where clause expression
* @param wildcardIncludesDynamicCols true if wildcard queries should include dynamic columns
* @return projector used to access row values during scan
* @throws SQLException
*/
public static RowProjector compile(StatementContext context, SelectStatement statement,
GroupBy groupBy, List<? extends PDatum> targetColumns, Expression where,
boolean wildcardIncludesDynamicCols) throws SQLException {
List<KeyValueColumnExpression> serverParsedKVRefs = new ArrayList<>();
List<ProjectedColumnExpression> serverParsedProjectedColumnRefs = new ArrayList<>();
List<Expression> serverParsedKVFuncs = new ArrayList<>();
List<Expression> serverParsedOldFuncs = new ArrayList<>();
Map<Expression, Integer> serverParsedExpressionCounts = new HashMap<>();
List<AliasedNode> aliasedNodes = statement.getSelect();
// Setup projected columns in Scan
SelectClauseVisitor
selectVisitor =
new SelectClauseVisitor(context, groupBy, serverParsedKVRefs, serverParsedKVFuncs,
serverParsedExpressionCounts, serverParsedProjectedColumnRefs,
serverParsedOldFuncs, statement);
List<ExpressionProjector> projectedColumns = new ArrayList<>();
ColumnResolver resolver = context.getResolver();
TableRef tableRef = context.getCurrentTable();
PTable table = tableRef.getTable();
boolean resolveColumn = !tableRef.equals(resolver.getTables().get(0));
boolean isWildcard = false;
Scan scan = context.getScan();
int index = 0;
List<Expression> projectedExpressions = Lists.newArrayListWithExpectedSize(aliasedNodes.size());
List<byte[]> projectedFamilies = Lists.newArrayListWithExpectedSize(aliasedNodes.size());
for (AliasedNode aliasedNode : aliasedNodes) {
ParseNode node = aliasedNode.getNode();
// TODO: visitor?
if (node instanceof WildcardParseNode) {
if (statement.isAggregate()) {
ExpressionCompiler.throwNonAggExpressionInAggException(node.toString());
}
if (tableRef == TableRef.EMPTY_TABLE_REF) {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.NO_TABLE_SPECIFIED_FOR_WILDCARD_SELECT).build().buildException();
}
isWildcard = true;
if (tableRef.getTable().getType() == PTableType.INDEX && ((WildcardParseNode)node).isRewrite()) {
projectAllIndexColumns(context, tableRef, resolveColumn, projectedExpressions, projectedColumns, targetColumns);
} else {
projectAllTableColumns(context, tableRef, resolveColumn, projectedExpressions, projectedColumns, targetColumns);
}
} else if (node instanceof TableWildcardParseNode) {
TableName tName = ((TableWildcardParseNode) node).getTableName();
TableRef tRef = resolver.resolveTable(tName.getSchemaName(), tName.getTableName());
if (tRef.equals(tableRef)) {
isWildcard = true;
}
if (tRef.getTable().getType() == PTableType.INDEX && ((TableWildcardParseNode)node).isRewrite()) {
projectAllIndexColumns(context, tRef, true, projectedExpressions, projectedColumns, targetColumns);
} else {
projectAllTableColumns(context, tRef, true, projectedExpressions, projectedColumns, targetColumns);
}
} else if (node instanceof FamilyWildcardParseNode) {
if (tableRef == TableRef.EMPTY_TABLE_REF) {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.NO_TABLE_SPECIFIED_FOR_WILDCARD_SELECT).build().buildException();
}
// Project everything for SELECT cf.*
String cfName = ((FamilyWildcardParseNode) node).getName();
// Delay projecting to scan, as when any other column in the column family gets
// added to the scan, it overwrites that we want to project the entire column
// family. Instead, we do the projection at the end.
// TODO: consider having a ScanUtil.addColumn and ScanUtil.addFamily to work
// around this, as this code depends on this function being the last place where
// columns are projected (which is currently true, but could change).
projectedFamilies.add(Bytes.toBytes(cfName));
if (tableRef.getTable().getType() == PTableType.INDEX && ((FamilyWildcardParseNode)node).isRewrite()) {
projectIndexColumnFamily(context, cfName, tableRef, resolveColumn, projectedExpressions, projectedColumns);
} else {
projectTableColumnFamily(context, cfName, tableRef, resolveColumn, projectedExpressions, projectedColumns);
}
} else {
if (node instanceof PhoenixRowTimestampParseNode) {
if (statement.isAggregate()) {
ExpressionCompiler.throwNonAggExpressionInAggException(node.toString());
}
}
Expression expression = node.accept(selectVisitor);
projectedExpressions.add(expression);
expression = coerceIfNecessary(index, targetColumns, expression);
if (node instanceof BindParseNode) {
context.getBindManager().addParamMetaData((BindParseNode)node, expression);
}
if (!node.isStateless()) {
if (!selectVisitor.isAggregate() && statement.isAggregate()) {
ExpressionCompiler.throwNonAggExpressionInAggException(expression.toString());
}
}
String tableName = tableRef.getTableAlias() == null ?
(table.getName() == null ?
"" :
table.getName().getString()) :
tableRef.getTableAlias();
String colName = SchemaUtil.normalizeIdentifier(aliasedNode.getNode().getAlias());
String name = colName == null ? expression.toString() : colName;
boolean isCaseSensitive = aliasedNode.getAlias() != null ?
aliasedNode.isCaseSensitve() :
(colName != null ?
SchemaUtil.isCaseSensitive(aliasedNode.getNode().getAlias()) :
selectVisitor.isCaseSensitive);
if (null != aliasedNode.getAlias()){
projectedColumns.add(new ExpressionProjector(name, aliasedNode.getAlias(), tableName, expression, isCaseSensitive));
} else {
projectedColumns.add(new ExpressionProjector(name, name, tableName, expression, isCaseSensitive));
}
}
selectVisitor.reset();
index++;
}
for (int i = serverParsedProjectedColumnRefs.size() - 1; i >= 0; i--) {
Expression expression = serverParsedProjectedColumnRefs.get(i);
Integer count = serverParsedExpressionCounts.get(expression);
if (count != 0) {
serverParsedKVRefs.remove(i);
serverParsedKVFuncs.remove(i);
serverParsedOldFuncs.remove(i);
}
}
if (serverParsedKVFuncs.size() > 0 && serverParsedKVRefs.size() > 0) {
String[]
scanAttributes =
new String[] { BaseScannerRegionObserverConstants.SPECIFIC_ARRAY_INDEX,
BaseScannerRegionObserverConstants.JSON_VALUE_FUNCTION,
BaseScannerRegionObserverConstants.JSON_QUERY_FUNCTION };
Map<String, Class> attributeToFunctionMap = new HashMap<String, Class>() {{
put(scanAttributes[0], ArrayIndexFunction.class);
put(scanAttributes[1], JsonValueFunction.class);
put(scanAttributes[2], JsonQueryFunction.class);
}};
// This map is to keep track of the positions that get swapped with rearranging
// the functions in the serialized data to server.
Map<Integer, Integer> initialToShuffledPositionMap = new HashMap<>();
Map<String, List<Expression>>
serverAttributeToFuncExpressionMap =
new HashMap<String, List<Expression>>() {{
for (String attribute : attributeToFunctionMap.keySet()) {
put(attribute, new ArrayList<>());
}
}};
Map<String, List<KeyValueColumnExpression>>
serverAttributeToKVExpressionMap =
new HashMap<String, List<KeyValueColumnExpression>>() {{
for (String attribute : attributeToFunctionMap.keySet()) {
put(attribute, new ArrayList<>());
}
}};
int counter = 0;
for (String attribute : scanAttributes) {
for (int i = 0; i < serverParsedKVFuncs.size(); i++) {
if (attributeToFunctionMap.get(attribute)
.isInstance(serverParsedKVFuncs.get(i))) {
initialToShuffledPositionMap.put(i, counter++);
serverAttributeToFuncExpressionMap.get(attribute)
.add(serverParsedKVFuncs.get(i));
serverAttributeToKVExpressionMap.get(attribute)
.add(serverParsedKVRefs.get(i));
}
}
}
for (Map.Entry<String, Class> entry : attributeToFunctionMap.entrySet()) {
if (serverAttributeToFuncExpressionMap.get(entry.getKey()).size() > 0) {
serializeServerParsedExpressionInformationAndSetInScan(context, entry.getKey(),
serverAttributeToFuncExpressionMap.get(entry.getKey()),
serverAttributeToKVExpressionMap.get(entry.getKey()));
}
}
KeyValueSchemaBuilder builder = new KeyValueSchemaBuilder(0);
for (Expression expression : serverParsedKVRefs) {
builder.addField(expression);
}
KeyValueSchema kvSchema = builder.build();
ValueBitSet arrayIndexesBitSet = ValueBitSet.newInstance(kvSchema);
builder = new KeyValueSchemaBuilder(0);
for (Expression expression : serverParsedKVFuncs) {
builder.addField(expression);
}
KeyValueSchema arrayIndexesSchema = builder.build();
Map<Expression, Expression> replacementMap = new HashMap<>();
for (int i = 0; i < serverParsedOldFuncs.size(); i++) {
Expression function = serverParsedKVFuncs.get(i);
replacementMap.put(serverParsedOldFuncs.get(i),
new ArrayIndexExpression(initialToShuffledPositionMap.get(i),
function.getDataType(), arrayIndexesBitSet, arrayIndexesSchema));
}
ReplaceArrayFunctionExpressionVisitor
visitor =
new ReplaceArrayFunctionExpressionVisitor(replacementMap);
for (int i = 0; i < projectedColumns.size(); i++) {
ExpressionProjector projector = projectedColumns.get(i);
projectedColumns.set(i,
new ExpressionProjector(projector.getName(), projector.getLabel(),
tableRef.getTableAlias() == null ?
(table.getName() == null ?
"" :
table.getName().getString()) :
tableRef.getTableAlias(),
projector.getExpression().accept(visitor),
projector.isCaseSensitive()));
}
}
boolean isProjectEmptyKeyValue = false;
// Don't project known/declared column families into the scan if we want to support
// surfacing dynamic columns in wildcard queries
if (isWildcard && !wildcardIncludesDynamicCols) {
projectAllColumnFamilies(table, scan);
} else {
isProjectEmptyKeyValue = where == null || LiteralExpression.isTrue(where) || where.requiresFinalEvaluation();
for (byte[] family : projectedFamilies) {
try {
if (table.getColumnFamily(family) != null) {
projectColumnFamily(table, scan, family);
}
} catch (ColumnFamilyNotFoundException e) {
if (!IndexUtil.shouldIndexBeUsedForUncoveredQuery(tableRef)) {
throw e;
}
}
}
}
// TODO make estimatedByteSize more accurate by counting the joined columns.
int estimatedKeySize = table.getRowKeySchema().getEstimatedValueLength();
int estimatedByteSize = 0;
for (Map.Entry<byte[],NavigableSet<byte[]>> entry : scan.getFamilyMap().entrySet()) {
try {
PColumnFamily family = table.getColumnFamily(entry.getKey());
if (entry.getValue() == null) {
for (PColumn column : family.getColumns()) {
Integer maxLength = column.getMaxLength();
int byteSize = column.getDataType().isFixedWidth() ? maxLength == null ? column.getDataType().getByteSize() : maxLength : RowKeySchema.ESTIMATED_VARIABLE_LENGTH_SIZE;
estimatedByteSize += SizedUtil.KEY_VALUE_SIZE + estimatedKeySize + byteSize;
}
} else {
for (byte[] cq : entry.getValue()) {
PColumn column = family.getPColumnForColumnQualifier(cq);
// Continue: If an EMPTY_COLUMN is in the projection list,
// since the table column list does not contain the EMPTY_COLUMN
// no value is returned.
if (column == null) {
continue;
}
Integer maxLength = column.getMaxLength();
int byteSize = column.getDataType().isFixedWidth() ? maxLength == null ? column.getDataType().getByteSize() : maxLength : RowKeySchema.ESTIMATED_VARIABLE_LENGTH_SIZE;
estimatedByteSize += SizedUtil.KEY_VALUE_SIZE + estimatedKeySize + byteSize;
}
}
} catch (ColumnFamilyNotFoundException e) {
// Ignore as this can happen for local indexes when the data table has a column family, but there are no covered columns in the family
}
}
return new RowProjector(projectedColumns, Math.max(estimatedKeySize, estimatedByteSize),
isProjectEmptyKeyValue, resolver.hasUDFs(), isWildcard,
wildcardIncludesDynamicCols);
}
private static void projectAllColumnFamilies(PTable table, Scan scan) {
// Will project all known/declared column families
scan.getFamilyMap().clear();
for (PColumnFamily family : table.getColumnFamilies()) {
scan.addFamily(family.getName().getBytes());
}
}
// A replaced ArrayIndex function that retrieves the exact array value retrieved from the server
static class ArrayIndexExpression extends BaseTerminalExpression {
private final int position;
private final PDataType type;
private final ValueBitSet arrayIndexesBitSet;
private final KeyValueSchema arrayIndexesSchema;
public ArrayIndexExpression(int position, PDataType type, ValueBitSet arrayIndexesBitSet, KeyValueSchema arrayIndexesSchema) {
this.position = position;
this.type = type;
this.arrayIndexesBitSet = arrayIndexesBitSet;
this.arrayIndexesSchema = arrayIndexesSchema;
}
@Override
public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
if (!tuple.getValue(QueryConstants.ARRAY_VALUE_COLUMN_FAMILY, QueryConstants.ARRAY_VALUE_COLUMN_QUALIFIER,
ptr)) {
return false;
}
int maxOffset = ptr.getOffset() + ptr.getLength();
arrayIndexesBitSet.or(ptr);
arrayIndexesSchema.iterator(ptr, position, arrayIndexesBitSet);
Boolean hasValue = arrayIndexesSchema.next(ptr, position, maxOffset, arrayIndexesBitSet);
arrayIndexesBitSet.clear();
if (hasValue == null) {
ptr.set(ByteUtil.EMPTY_BYTE_ARRAY);
}
return true;
}
@Override
public PDataType getDataType() {
return this.type;
}
@Override
public <T> T accept(ExpressionVisitor<T> visitor) {
// TODO Auto-generated method stub
return null;
}
}
private static void serializeServerParsedExpressionInformationAndSetInScan(
StatementContext context, String serverParsedExpressionAttribute,
List<Expression> serverParsedKVFuncs,
List<KeyValueColumnExpression> serverParsedKVRefs) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
DataOutputStream output = new DataOutputStream(stream);
// Write the KVRef size followed by the keyvalues that needs to be of
// type arrayindex or json function based on serverParsedExpressionAttribute
WritableUtils.writeVInt(output, serverParsedKVRefs.size());
for (Expression expression : serverParsedKVRefs) {
expression.write(output);
}
// then write the number of arrayindex or json functions followed
// by the expression itself
WritableUtils.writeVInt(output, serverParsedKVFuncs.size());
for (Expression expression : serverParsedKVFuncs) {
expression.write(output);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
stream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
context.getScan().setAttribute(serverParsedExpressionAttribute, stream.toByteArray());
}
private static class SelectClauseVisitor extends ExpressionCompiler {
/**
* Track whether or not the projection expression is case sensitive. We use this
* information to determine whether or not we normalize the column name passed
*/
private boolean isCaseSensitive;
private int elementCount;
// Looks at PHOENIX-2160 for the context and use of the below variables.
// These are used for reference counting and converting to KeyValueColumnExpressions
private List<KeyValueColumnExpression> serverParsedKVRefs;
private List<Expression> serverParsedKVFuncs;
private List<Expression> serverParsedOldFuncs;
private List<ProjectedColumnExpression> serverParsedProjectedColumnRefs;
private Map<Expression, Integer> serverParsedExpressionCounts;
private SelectStatement statement;
private SelectClauseVisitor(StatementContext context, GroupBy groupBy,
List<KeyValueColumnExpression> serverParsedKVRefs,
List<Expression> serverParsedKVFuncs,
Map<Expression, Integer> serverParsedExpressionCounts,
List<ProjectedColumnExpression> serverParsedProjectedColumnRefs,
List<Expression> serverParsedOldFuncs, SelectStatement statement) {
super(context, groupBy);
this.serverParsedKVRefs = serverParsedKVRefs;
this.serverParsedKVFuncs = serverParsedKVFuncs;
this.serverParsedOldFuncs = serverParsedOldFuncs;
this.serverParsedExpressionCounts = serverParsedExpressionCounts;
this.serverParsedProjectedColumnRefs = serverParsedProjectedColumnRefs;
this.statement = statement;
reset();
}
@Override
public void reset() {
super.reset();
elementCount = 0;
isCaseSensitive = true;
}
@Override
protected ColumnRef resolveColumn(ColumnParseNode node) throws SQLException {
ColumnRef ref = super.resolveColumn(node);
isCaseSensitive = isCaseSensitive && node.isCaseSensitive();
return ref;
}
@Override
public Expression visit(ColumnParseNode node) throws SQLException {
Expression expression = super.visit(node);
if (parseOnServer(expression)) {
Integer count = serverParsedExpressionCounts.get(expression);
serverParsedExpressionCounts.put(expression, count != null ? (count + 1) : 1);
}
return expression;
}
private static boolean parseOnServer(Expression expression) {
return expression.getDataType().isArrayType() || expression.getDataType()
.equals(PJson.INSTANCE);
}
@Override
public void addElement(List<Expression> l, Expression element) {
elementCount++;
isCaseSensitive &= elementCount == 1;
super.addElement(l, element);
}
@Override
public Expression visit(SequenceValueParseNode node) throws SQLException {
if (aggregateFunction != null) {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.INVALID_USE_OF_NEXT_VALUE_FOR)
.setSchemaName(node.getTableName().getSchemaName())
.setTableName(node.getTableName().getTableName()).build().buildException();
}
return context.getSequenceManager().newSequenceReference(node);
}
@Override
public Expression visitLeave(FunctionParseNode node, final List<Expression> children) throws SQLException {
// this need not be done for group by clause with array or json. Hence, the below check
if (!statement.isAggregate() && (ArrayIndexFunction.NAME.equals(
node.getName()) || isJsonFunction(node)) &&
children.get(0) instanceof ProjectedColumnExpression) {
final List<KeyValueColumnExpression> indexKVs = Lists.newArrayList();
final List<ProjectedColumnExpression> indexProjectedColumns = Lists.newArrayList();
final List<Expression> copyOfChildren = new ArrayList<>(children);
// Create anon visitor to find reference to array or json in a generic way
children.get(0).accept(new ProjectedColumnExpressionVisitor() {
@Override
public Void visit(ProjectedColumnExpression expression) {
if (expression.getDataType().isArrayType() || expression.getDataType()
.equals(PJson.INSTANCE)) {
indexProjectedColumns.add(expression);
PColumn col = expression.getColumn();
// hack'ish... For covered columns with local indexes we defer to the server.
if (col instanceof ProjectedColumn && ((ProjectedColumn) col).getSourceColumnRef() instanceof IndexUncoveredDataColumnRef) {
return null;
}
PTable table = context.getCurrentTable().getTable();
KeyValueColumnExpression keyValueColumnExpression;
if (table.getImmutableStorageScheme() != ImmutableStorageScheme.ONE_CELL_PER_COLUMN) {
keyValueColumnExpression =
new SingleCellColumnExpression(col,
col.getName().getString(),
table.getEncodingScheme(),
table.getImmutableStorageScheme());
} else {
keyValueColumnExpression = new KeyValueColumnExpression(col);
}
indexKVs.add(keyValueColumnExpression);
copyOfChildren.set(0, keyValueColumnExpression);
Integer count = serverParsedExpressionCounts.get(expression);
serverParsedExpressionCounts.put(expression,
count != null ? (count - 1) : -1);
}
return null;
}
});
Expression func = super.visitLeave(node, children);
// Add the keyvalues which is of type array or json
if (!indexKVs.isEmpty()) {
serverParsedKVRefs.addAll(indexKVs);
serverParsedProjectedColumnRefs.addAll(indexProjectedColumns);
Expression funcModified = super.visitLeave(node, copyOfChildren);
// Track the array index or json function also
serverParsedKVFuncs.add(funcModified);
serverParsedOldFuncs.add(func);
}
return func;
} else {
return super.visitLeave(node, children);
}
}
}
private static boolean isJsonFunction(FunctionParseNode node) {
return JsonValueFunction.NAME.equals(node.getName()) || JsonQueryFunction.NAME.equals(
node.getName());
}
}