/* * 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 net.hydromatic.optiq.impl.splunk; import net.hydromatic.optiq.impl.splunk.util.StringUtils; import org.eigenbase.rel.*; import org.eigenbase.relopt.*; import org.eigenbase.reltype.*; import org.eigenbase.rex.*; import org.eigenbase.sql.*; import org.eigenbase.sql.fun.SqlStdOperatorTable; import org.eigenbase.sql.type.SqlTypeName; import org.eigenbase.util.NlsString; import org.eigenbase.util.Pair; import com.google.common.collect.ImmutableSet; import java.util.*; import java.util.logging.Logger; /** * Planner rule to push filters and projections to Splunk. */ public class SplunkPushDownRule extends RelOptRule { private static final Logger LOGGER = StringUtils.getClassTracer(SplunkPushDownRule.class); private static final Set<SqlKind> SUPPORTED_OPS = ImmutableSet.of( SqlKind.CAST, SqlKind.EQUALS, SqlKind.LESS_THAN, SqlKind.LESS_THAN_OR_EQUAL, SqlKind.GREATER_THAN, SqlKind.GREATER_THAN_OR_EQUAL, SqlKind.NOT_EQUALS, SqlKind.LIKE, SqlKind.AND, SqlKind.OR, SqlKind.NOT); public static final SplunkPushDownRule PROJECT_ON_FILTER = new SplunkPushDownRule( operand( ProjectRel.class, operand( FilterRel.class, operand( ProjectRel.class, operand(SplunkTableAccessRel.class, none())))), "proj on filter on proj"); public static final SplunkPushDownRule FILTER_ON_PROJECT = new SplunkPushDownRule( operand( FilterRel.class, operand( ProjectRel.class, operand(SplunkTableAccessRel.class, none()))), "filter on proj"); public static final SplunkPushDownRule FILTER = new SplunkPushDownRule( operand( FilterRel.class, operand(SplunkTableAccessRel.class, none())), "filter"); public static final SplunkPushDownRule PROJECT = new SplunkPushDownRule( operand( ProjectRel.class, operand(SplunkTableAccessRel.class, none())), "proj"); /** Creates a SplunkPushDownRule. */ protected SplunkPushDownRule(RelOptRuleOperand rule, String id) { super(rule, "SplunkPushDownRule: " + id); } // ~ Methods -------------------------------------------------------------- // implement RelOptRule public void onMatch(RelOptRuleCall call) { LOGGER.fine(description); int relLength = call.rels.length; SplunkTableAccessRel splunkRel = (SplunkTableAccessRel) call.rels[relLength - 1]; FilterRel filter; ProjectRel topProj = null; ProjectRel bottomProj = null; RelDataType topRow = splunkRel.getRowType(); int filterIdx = 2; if (call.rels[relLength - 2] instanceof ProjectRel) { bottomProj = (ProjectRel) call.rels[relLength - 2]; filterIdx = 3; // bottom projection will change the field count/order topRow = bottomProj.getRowType(); } String filterString; if (filterIdx <= relLength && call.rels[relLength - filterIdx] instanceof FilterRel) { filter = (FilterRel) call.rels[relLength - filterIdx]; int topProjIdx = filterIdx + 1; if (topProjIdx <= relLength && call.rels[relLength - topProjIdx] instanceof ProjectRel) { topProj = (ProjectRel) call.rels[relLength - topProjIdx]; } RexCall filterCall = (RexCall) filter.getCondition(); SqlOperator op = filterCall.getOperator(); List<RexNode> operands = filterCall.getOperands(); LOGGER.fine("fieldNames: " + getFieldsString(topRow)); final StringBuilder buf = new StringBuilder(); if (getFilter(op, operands, buf, topRow.getFieldNames())) { filterString = buf.toString(); } else { return; // can't handle } } else { filterString = ""; } // top projection will change the field count/order if (topProj != null) { topRow = topProj.getRowType(); } LOGGER.fine("pre transformTo fieldNames: " + getFieldsString(topRow)); call.transformTo( appendSearchString( filterString, splunkRel, topProj, bottomProj, topRow, null)); } /** * Appends a search string. * * @param toAppend Search string to append * @param splunkRel Relational expression * @param topProj Top projection * @param bottomProj Bottom projection */ protected RelNode appendSearchString( String toAppend, SplunkTableAccessRel splunkRel, ProjectRel topProj, ProjectRel bottomProj, RelDataType topRow, RelDataType bottomRow) { StringBuilder updateSearchStr = new StringBuilder(splunkRel.search); if (!toAppend.isEmpty()) { updateSearchStr.append(" ").append(toAppend); } List<RelDataTypeField> bottomFields = bottomRow == null ? null : bottomRow.getFieldList(); List<RelDataTypeField> topFields = topRow == null ? null : topRow.getFieldList(); if (bottomFields == null) { bottomFields = splunkRel.getRowType().getFieldList(); } // handle bottom projection (ie choose a subset of the table fields) if (bottomProj != null) { List<RelDataTypeField> tmp = new ArrayList<RelDataTypeField>(); List<RelDataTypeField> dRow = bottomProj.getRowType().getFieldList(); for (RexNode rn : bottomProj.getProjects()) { RelDataTypeField rdtf; if (rn instanceof RexSlot) { RexSlot rs = (RexSlot) rn; rdtf = bottomFields.get(rs.getIndex()); } else { rdtf = dRow.get(tmp.size()); } tmp.add(rdtf); } bottomFields = tmp; } // field renaming: to -> from List<Pair<String, String>> renames = new LinkedList<Pair<String, String>>(); // handle top projection (ie reordering and renaming) List<RelDataTypeField> newFields = bottomFields; if (topProj != null) { LOGGER.fine("topProj: " + String.valueOf(topProj.getPermutation())); newFields = new ArrayList<RelDataTypeField>(); int i = 0; for (RexNode rn : topProj.getProjects()) { RexInputRef rif = (RexInputRef) rn; RelDataTypeField field = bottomFields.get(rif.getIndex()); if (!bottomFields.get(rif.getIndex()).getName() .equals(topFields.get(i).getName())) { renames.add( new Pair<String, String>( bottomFields.get(rif.getIndex()).getName(), topFields.get(i).getName())); field = topFields.get(i); } newFields.add(field); } } if (!renames.isEmpty()) { updateSearchStr.append("| rename "); for (Pair<String, String> p : renames) { updateSearchStr.append(p.left).append(" AS ") .append(p.right).append(" "); } } RelDataType resultType = new RelRecordType(newFields); String searchWithFilter = updateSearchStr.toString(); RelNode rel = new SplunkTableAccessRel( splunkRel.getCluster(), splunkRel.getTable(), splunkRel.splunkTable, searchWithFilter, splunkRel.earliest, splunkRel.latest, resultType.getFieldNames()); LOGGER.fine( "end of appendSearchString fieldNames: " + rel.getRowType().getFieldNames()); return rel; } // ~ Private Methods ------------------------------------------------------ private static RelNode addProjectionRule(ProjectRel proj, RelNode rel) { if (proj == null) { return rel; } return new ProjectRel( proj.getCluster(), proj.getCluster().traitSetOf( proj.getCollationList().isEmpty() ? RelCollationImpl.EMPTY : proj.getCollationList().get(0)), rel, proj.getProjects(), proj.getRowType(), proj.getFlags()); } // TODO: use StringBuilder instead of String // TODO: refactor this to use more tree like parsing, need to also // make sure we use parens properly - currently precedence // rules are simply left to right private boolean getFilter(SqlOperator op, List<RexNode> operands, StringBuilder s, List<String> fieldNames) { if (!valid(op.getKind())) { return false; } boolean like = false; switch (op.getKind()) { case NOT: // NOT op pre-pended s = s.append(" NOT "); break; case CAST: return asd(false, operands, s, fieldNames, 0); case LIKE: like = true; break; } for (int i = 0; i < operands.size(); i++) { if (!asd(like, operands, s, fieldNames, i)) { return false; } if (op instanceof SqlBinaryOperator && i == 0) { s.append(" ").append(op).append(" "); } } return true; } private boolean asd(boolean like, List<RexNode> operands, StringBuilder s, List<String> fieldNames, int i) { RexNode operand = operands.get(i); if (operand instanceof RexCall) { s.append("("); final RexCall call = (RexCall) operand; boolean b = getFilter( call.getOperator(), call.getOperands(), s, fieldNames); if (!b) { return false; } s.append(")"); } else { if (operand instanceof RexInputRef) { if (i != 0) { return false; } int fieldIndex = ((RexInputRef) operand).getIndex(); String name = fieldNames.get(fieldIndex); s.append(name); } else { // RexLiteral String tmp = toString(like, (RexLiteral) operand); if (tmp == null) { return false; } s.append(tmp); } } return true; } private boolean valid(SqlKind kind) { return SUPPORTED_OPS.contains(kind); } private String toString(SqlOperator op) { if (op.equals(SqlStdOperatorTable.LIKE)) { return SqlStdOperatorTable.EQUALS.toString(); } else if (op.equals(SqlStdOperatorTable.NOT_EQUALS)) { return "!="; } return op.toString(); } public static String searchEscape(String str) { if (str.isEmpty()) { return "\"\""; } StringBuilder sb = new StringBuilder(str.length()); boolean quote = false; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c == '"' || c == '\\') { sb.append('\\'); } sb.append(c); quote |= !(Character.isLetterOrDigit(c) || c == '_'); } if (quote || sb.length() != str.length()) { sb.insert(0, '"'); sb.append('"'); return sb.toString(); } return str; } private String toString(boolean like, RexLiteral literal) { String value = null; SqlTypeName litSqlType = literal.getTypeName(); if (SqlTypeName.NUMERIC_TYPES.contains(litSqlType)) { value = literal.getValue().toString(); } else if (litSqlType.equals(SqlTypeName.CHAR)) { value = ((NlsString) literal.getValue()).getValue(); if (like) { value = value.replaceAll("%", "*"); } value = searchEscape(value); } return value; } // transform the call from SplunkUdxRel to FarragoJavaUdxRel // usually used to stop the optimizer from calling us protected void transformToFarragoUdxRel( RelOptRuleCall call, SplunkTableAccessRel splunkRel, FilterRel filter, ProjectRel topProj, ProjectRel bottomProj) { assert false; /* RelNode rel = new JavaRules.EnumerableTableAccessRel( udxRel.getCluster(), udxRel.getTable(), udxRel.getRowType(), udxRel.getServerMofId()); rel = RelOptUtil.createCastRel(rel, udxRel.getRowType(), true); rel = addProjectionRule(bottomProj, rel); if (filter != null) { rel = new FilterRel(filter.getCluster(), rel, filter.getCondition()); } rel = addProjectionRule(topProj, rel); call.transformTo(rel); */ } public static String getFieldsString(RelDataType row) { return row.getFieldNames().toString(); } } // End SplunkPushDownRule.java