@Override
  public boolean rewritePost(Mutable<ILogicalOperator> opRef, IOptimizationContext context)
      throws AlgebricksException {
    AbstractLogicalOperator op = (AbstractLogicalOperator) opRef.getValue();

    if (op.getOperatorTag() != LogicalOperatorTag.INNERJOIN) {
      return false;
    }
    AbstractBinaryJoinOperator join = (AbstractBinaryJoinOperator) op;

    ILogicalExpression expr = join.getCondition().getValue();
    if (expr.getExpressionTag() != LogicalExpressionTag.FUNCTION_CALL) {
      return false;
    }
    AbstractFunctionCallExpression fexp = (AbstractFunctionCallExpression) expr;
    FunctionIdentifier fi = fexp.getFunctionIdentifier();
    if (!fi.equals(AlgebricksBuiltinFunctions.AND)) {
      return false;
    }
    List<Mutable<ILogicalExpression>> eqVarVarComps = new ArrayList<Mutable<ILogicalExpression>>();
    List<Mutable<ILogicalExpression>> otherPredicates =
        new ArrayList<Mutable<ILogicalExpression>>();
    for (Mutable<ILogicalExpression> arg : fexp.getArguments()) {
      if (isEqVarVar(arg.getValue())) {
        eqVarVarComps.add(arg);
      } else {
        otherPredicates.add(arg);
      }
    }
    if (eqVarVarComps.isEmpty() || otherPredicates.isEmpty()) {
      return false;
    }
    // pull up
    ILogicalExpression pulledCond = makeCondition(otherPredicates, context);
    SelectOperator select =
        new SelectOperator(new MutableObject<ILogicalExpression>(pulledCond), false, null);
    ILogicalExpression newJoinCond = makeCondition(eqVarVarComps, context);
    join.getCondition().setValue(newJoinCond);
    select.getInputs().add(new MutableObject<ILogicalOperator>(join));
    opRef.setValue(select);
    context.computeAndSetTypeEnvironmentForOperator(select);
    return true;
  }
  @SuppressWarnings("unchecked")
  @Override
  public void evaluate(IFrameTupleReference tuple) throws AlgebricksException {
    array0.reset();
    eval0.evaluate(tuple);
    array1.reset();
    eval1.evaluate(tuple);
    array2.reset();
    eval2.evaluate(tuple);

    try {
      if (array0.getByteArray()[0] == SER_NULL_TYPE_TAG
          || array1.getByteArray()[0] == SER_NULL_TYPE_TAG
          || array2.getByteArray()[0] == SER_NULL_TYPE_TAG) {
        nullSerde.serialize(ANull.NULL, dout);
        return;
      } else if (array0.getByteArray()[0] != SER_STRING_TYPE_TAG
          || array1.getByteArray()[0] != SER_STRING_TYPE_TAG
          || array2.getByteArray()[0] != SER_STRING_TYPE_TAG) {
        throw new AlgebricksException(
            funcID.getName()
                + ": expects input type (STRING/NULL, STRING/NULL, STRING/NULL), but got ("
                + EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(array0.getByteArray()[0])
                + ", "
                + EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(array1.getByteArray()[0])
                + ", "
                + EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(array2.getByteArray()[0])
                + ".");
      }
    } catch (HyracksDataException e) {
      throw new AlgebricksException(e);
    }

    strPtr1st.set(array0.getByteArray(), array0.getStartOffset() + 1, array0.getLength());
    strPtr2nd.set(array1.getByteArray(), array1.getStartOffset() + 1, array1.getLength());
    strPtr3rd.set(array2.getByteArray(), array2.getStartOffset() + 1, array2.getLength());

    String res = compute(strPtr1st, strPtr2nd, strPtr3rd);
    resultBuffer.setValue(res);
    try {
      strSerde.serialize(resultBuffer, dout);
    } catch (HyracksDataException e) {
      throw new AlgebricksException(e);
    }
  }
  @Override
  public boolean rewritePost(Mutable<ILogicalOperator> opRef, IOptimizationContext context)
      throws AlgebricksException {
    AbstractLogicalOperator op = (AbstractLogicalOperator) opRef.getValue();
    // current opperator is join
    if (op.getOperatorTag() != LogicalOperatorTag.INNERJOIN
        && op.getOperatorTag() != LogicalOperatorTag.LEFTOUTERJOIN) {
      return false;
    }

    // Find GET_ITEM function.
    AbstractBinaryJoinOperator joinOp = (AbstractBinaryJoinOperator) op;
    Mutable<ILogicalExpression> expRef = joinOp.getCondition();
    Mutable<ILogicalExpression> getItemExprRef = getSimilarityExpression(expRef);
    if (getItemExprRef == null) {
      return false;
    }
    // Check if the GET_ITEM function is on one of the supported similarity-check functions.
    AbstractFunctionCallExpression getItemFuncExpr =
        (AbstractFunctionCallExpression) getItemExprRef.getValue();
    Mutable<ILogicalExpression> argRef = getItemFuncExpr.getArguments().get(0);
    AbstractFunctionCallExpression simFuncExpr = (AbstractFunctionCallExpression) argRef.getValue();
    if (!simFuncs.contains(simFuncExpr.getFunctionIdentifier())) {
      return false;
    }
    // Skip this rule based on annotations.
    if (simFuncExpr.getAnnotations().containsKey(IndexedNLJoinExpressionAnnotation.INSTANCE)) {
      return false;
    }

    List<Mutable<ILogicalOperator>> inputOps = joinOp.getInputs();
    ILogicalOperator leftInputOp = inputOps.get(0).getValue();
    ILogicalOperator rightInputOp = inputOps.get(1).getValue();

    List<Mutable<ILogicalExpression>> inputExps = simFuncExpr.getArguments();

    ILogicalExpression inputExp0 = inputExps.get(0).getValue();
    ILogicalExpression inputExp1 = inputExps.get(1).getValue();

    // left and right expressions are variables
    if (inputExp0.getExpressionTag() != LogicalExpressionTag.VARIABLE
        || inputExp1.getExpressionTag() != LogicalExpressionTag.VARIABLE) {
      return false;
    }

    LogicalVariable inputVar0 = ((VariableReferenceExpression) inputExp0).getVariableReference();
    LogicalVariable inputVar1 = ((VariableReferenceExpression) inputExp1).getVariableReference();

    LogicalVariable leftInputVar;
    LogicalVariable rightInputVar;

    liveVars.clear();
    VariableUtilities.getLiveVariables(leftInputOp, liveVars);
    if (liveVars.contains(inputVar0)) {
      leftInputVar = inputVar0;
      rightInputVar = inputVar1;
    } else {
      leftInputVar = inputVar1;
      rightInputVar = inputVar0;
    }

    List<LogicalVariable> leftInputPKs = context.findPrimaryKey(leftInputVar);
    List<LogicalVariable> rightInputPKs = context.findPrimaryKey(rightInputVar);
    // Bail if primary keys could not be inferred.
    if (leftInputPKs == null || rightInputPKs == null) {
      return false;
    }
    // primary key has only one variable
    if (leftInputPKs.size() != 1 || rightInputPKs.size() != 1) {
      return false;
    }
    IAType leftType =
        (IAType) context.getOutputTypeEnvironment(leftInputOp).getVarType(leftInputVar);
    IAType rightType =
        (IAType) context.getOutputTypeEnvironment(rightInputOp).getVarType(rightInputVar);
    // left-hand side and right-hand side of "~=" has the same type
    IAType left2 = TypeHelper.getNonOptionalType(leftType);
    IAType right2 = TypeHelper.getNonOptionalType(rightType);
    if (!left2.deepEqual(right2)) {
      return false;
    }
    //
    // -- - FIRE - --
    //
    AqlMetadataProvider metadataProvider = ((AqlMetadataProvider) context.getMetadataProvider());
    FunctionIdentifier funcId = FuzzyUtils.getTokenizer(leftType.getTypeTag());
    String tokenizer;
    if (funcId == null) {
      tokenizer = "";
    } else {
      tokenizer = funcId.getName();
    }

    float simThreshold = FuzzyUtils.getSimThreshold(metadataProvider);
    String simFunction = FuzzyUtils.getSimFunction(metadataProvider);

    // finalize AQL+ query
    String prepareJoin;
    switch (joinOp.getJoinKind()) {
      case INNER:
        {
          prepareJoin = "join" + AQLPLUS;
          break;
        }
      case LEFT_OUTER:
        {
          // TODO To make it work for Left Outer Joins, we should permute
          // the #LEFT and #RIGHT at the top of the AQL+ query. But, when
          // doing this, the
          // fuzzyjoin/user-vis-int-vis-user-lot-aqlplus_1.aql (the one
          // doing 3-way fuzzy joins) gives a different result. But even
          // if we don't change the FuzzyJoinRule, permuting the for
          // clauses in fuzzyjoin/user-vis-int-vis-user-lot-aqlplus_1.aql
          // leads to different results, which suggests there is some
          // other sort of bug.
          return false;
          // prepareJoin = "loj" + AQLPLUS;
          // break;
        }
      default:
        {
          throw new IllegalStateException();
        }
    }
    String aqlPlus =
        String.format(
            Locale.US,
            prepareJoin,
            tokenizer,
            tokenizer,
            simFunction,
            simThreshold,
            tokenizer,
            tokenizer,
            simFunction,
            simThreshold,
            simFunction,
            simThreshold,
            simThreshold);

    LogicalVariable leftPKVar = leftInputPKs.get(0);
    LogicalVariable rightPKVar = rightInputPKs.get(0);

    Counter counter = new Counter(context.getVarCounter());

    AQLPlusParser parser = new AQLPlusParser(new StringReader(aqlPlus));
    parser.initScope();
    parser.setVarCounter(counter);
    List<Clause> clauses;
    try {
      clauses = parser.Clauses();
    } catch (ParseException e) {
      throw new AlgebricksException(e);
    }
    // The translator will compile metadata internally. Run this compilation
    // under the same transaction id as the "outer" compilation.
    AqlPlusExpressionToPlanTranslator translator =
        new AqlPlusExpressionToPlanTranslator(
            metadataProvider.getJobId(), metadataProvider, counter, null, null);
    context.setVarCounter(counter.get());

    LogicalOperatorDeepCopyWithNewVariablesVisitor deepCopyVisitor =
        new LogicalOperatorDeepCopyWithNewVariablesVisitor(context);

    translator.addOperatorToMetaScope(new Identifier("#LEFT"), leftInputOp);
    translator.addVariableToMetaScope(new Identifier("$$LEFT"), leftInputVar);
    translator.addVariableToMetaScope(new Identifier("$$LEFTPK"), leftPKVar);

    translator.addOperatorToMetaScope(new Identifier("#RIGHT"), rightInputOp);
    translator.addVariableToMetaScope(new Identifier("$$RIGHT"), rightInputVar);
    translator.addVariableToMetaScope(new Identifier("$$RIGHTPK"), rightPKVar);

    translator.addOperatorToMetaScope(
        new Identifier("#LEFT_1"), deepCopyVisitor.deepCopy(leftInputOp, null));
    translator.addVariableToMetaScope(
        new Identifier("$$LEFT_1"), deepCopyVisitor.varCopy(leftInputVar));
    translator.addVariableToMetaScope(
        new Identifier("$$LEFTPK_1"), deepCopyVisitor.varCopy(leftPKVar));
    deepCopyVisitor.updatePrimaryKeys(context);
    deepCopyVisitor.reset();

    // translator.addOperatorToMetaScope(new Identifier("#LEFT_2"),
    // deepCopyVisitor.deepCopy(leftInputOp, null));
    // translator.addVariableToMetaScope(new Identifier("$$LEFT_2"),
    // deepCopyVisitor.varCopy(leftInputVar));
    // translator.addVariableToMetaScope(new Identifier("$$LEFTPK_2"),
    // deepCopyVisitor.varCopy(leftPKVar));
    // deepCopyVisitor.updatePrimaryKeys(context);
    // deepCopyVisitor.reset();
    //
    // translator.addOperatorToMetaScope(new Identifier("#LEFT_3"),
    // deepCopyVisitor.deepCopy(leftInputOp, null));
    // translator.addVariableToMetaScope(new Identifier("$$LEFT_3"),
    // deepCopyVisitor.varCopy(leftInputVar));
    // translator.addVariableToMetaScope(new Identifier("$$LEFTPK_3"),
    // deepCopyVisitor.varCopy(leftPKVar));
    // deepCopyVisitor.updatePrimaryKeys(context);
    // deepCopyVisitor.reset();

    translator.addOperatorToMetaScope(
        new Identifier("#RIGHT_1"), deepCopyVisitor.deepCopy(rightInputOp, null));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHT_1"), deepCopyVisitor.varCopy(rightInputVar));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHTPK_1"), deepCopyVisitor.varCopy(rightPKVar));
    deepCopyVisitor.updatePrimaryKeys(context);
    deepCopyVisitor.reset();

    // TODO pick side to run Stage 1, currently always picks RIGHT side
    translator.addOperatorToMetaScope(
        new Identifier("#RIGHT_2"), deepCopyVisitor.deepCopy(rightInputOp, null));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHT_2"), deepCopyVisitor.varCopy(rightInputVar));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHTPK_2"), deepCopyVisitor.varCopy(rightPKVar));
    deepCopyVisitor.updatePrimaryKeys(context);
    deepCopyVisitor.reset();

    translator.addOperatorToMetaScope(
        new Identifier("#RIGHT_3"), deepCopyVisitor.deepCopy(rightInputOp, null));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHT_3"), deepCopyVisitor.varCopy(rightInputVar));
    translator.addVariableToMetaScope(
        new Identifier("$$RIGHTPK_3"), deepCopyVisitor.varCopy(rightPKVar));
    deepCopyVisitor.updatePrimaryKeys(context);
    deepCopyVisitor.reset();

    ILogicalPlan plan;
    try {
      plan = translator.translate(clauses);
    } catch (AsterixException e) {
      throw new AlgebricksException(e);
    }
    context.setVarCounter(counter.get());

    ILogicalOperator outputOp = plan.getRoots().get(0).getValue();

    SelectOperator extraSelect = null;
    if (getItemExprRef != expRef) {
      // more than one join condition
      getItemExprRef.setValue(ConstantExpression.TRUE);
      switch (joinOp.getJoinKind()) {
        case INNER:
          {
            extraSelect = new SelectOperator(expRef, false, null);
            extraSelect.getInputs().add(new MutableObject<ILogicalOperator>(outputOp));
            outputOp = extraSelect;
            break;
          }
        case LEFT_OUTER:
          {
            if (((AbstractLogicalOperator) outputOp).getOperatorTag()
                != LogicalOperatorTag.LEFTOUTERJOIN) {
              throw new IllegalStateException();
            }
            LeftOuterJoinOperator topJoin = (LeftOuterJoinOperator) outputOp;
            topJoin.getCondition().setValue(expRef.getValue());
            break;
          }
        default:
          {
            throw new IllegalStateException();
          }
      }
    }
    opRef.setValue(outputOp);
    OperatorPropertiesUtil.typeOpRec(opRef, context);
    return true;
  }