/**
   * Method to return an expression for Map.containsValue using a subquery "EXISTS". This is for use
   * when there are "!contains" or "OR" operations in the filter. Creates the following SQL,
   *
   * <ul>
   *   <li><b>Map using join table</b>
   *       <pre>
   * SELECT 1 FROM JOIN_TBL A0_SUB
   * WHERE A0_SUB.JOIN_OWN_ID = A0.ID AND A0_SUB.JOIN_VAL_ID = {valExpr}
   * </pre>
   *   <li><b>Map with key stored in value</b>
   *       <pre>
   * SELECT 1 FROM VAL_TABLE A0_SUB INNER JOIN KEY_TBL B0 ON ...
   * WHERE B0.JOIN_OWN_ID = A0.ID AND A0_SUB.ID = {valExpr}
   * </pre>
   *   <li><b>Map of value stored in key</b>
   *       <pre>
   * SELECT 1 FROM VAL_TABLE A0_SUB
   * WHERE A0_SUB.OWN_ID = A0.ID AND A0_SUB.ID = {valExpr}
   * </pre>
   * </ul>
   *
   * and returns a BooleanSubqueryExpression ("EXISTS (subquery)")
   *
   * @param mapExpr Map expression
   * @param valExpr Expression for the value
   * @return Contains expression
   */
  protected SQLExpression containsAsSubquery(MapExpression mapExpr, SQLExpression valExpr) {
    boolean valIsUnbound = (valExpr instanceof UnboundExpression);
    String varName = null;
    if (valIsUnbound) {
      varName = ((UnboundExpression) valExpr).getVariableName();
      NucleusLogger.QUERY.debug(
          "map.containsValue binding unbound variable " + varName + " using SUBQUERY");
      // TODO What if the variable is declared as a subtype, handle this see
      // CollectionContainsMethod
    }

    RDBMSStoreManager storeMgr = stmt.getRDBMSManager();
    MetaDataManager mmgr = storeMgr.getMetaDataManager();
    AbstractMemberMetaData mmd = mapExpr.getJavaTypeMapping().getMemberMetaData();
    AbstractClassMetaData valCmd = mmd.getMap().getValueClassMetaData(clr, mmgr);
    MapTable joinTbl = (MapTable) storeMgr.getTable(mmd);
    SQLStatement subStmt = null;
    if (mmd.getMap().getMapType() == MapType.MAP_TYPE_JOIN) {
      // JoinTable Map
      if (valCmd == null) {
        // Map<?, Non-PC>
        subStmt = new SQLStatement(stmt, storeMgr, joinTbl, null, null);
        subStmt.setClassLoaderResolver(clr);
        JavaTypeMapping oneMapping = storeMgr.getMappingManager().getMapping(Integer.class);
        subStmt.select(exprFactory.newLiteral(subStmt, oneMapping, 1), null);

        // Restrict to map owner
        JavaTypeMapping ownerMapping = ((JoinTable) joinTbl).getOwnerMapping();
        SQLExpression ownerExpr =
            exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), ownerMapping);
        SQLExpression ownerIdExpr =
            exprFactory.newExpression(
                stmt, mapExpr.getSQLTable(), mapExpr.getSQLTable().getTable().getIdMapping());
        subStmt.whereAnd(ownerExpr.eq(ownerIdExpr), true);

        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr =
              exprFactory.newExpression(
                  subStmt, subStmt.getPrimaryTable(), joinTbl.getValueMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, null, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(
                  subStmt, subStmt.getPrimaryTable(), joinTbl.getValueMapping());
          subStmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      } else {
        // Map<?, PC>
        DatastoreClass valTbl = storeMgr.getDatastoreClass(mmd.getMap().getValueType(), clr);
        subStmt = new SQLStatement(stmt, storeMgr, valTbl, null, null);
        subStmt.setClassLoaderResolver(clr);
        JavaTypeMapping oneMapping = storeMgr.getMappingManager().getMapping(Integer.class);
        subStmt.select(exprFactory.newLiteral(subStmt, oneMapping, 1), null);

        // Join to join table
        SQLTable joinSqlTbl =
            subStmt.innerJoin(
                subStmt.getPrimaryTable(),
                valTbl.getIdMapping(),
                joinTbl,
                null,
                joinTbl.getValueMapping(),
                null,
                null);

        // Restrict to map owner
        JavaTypeMapping ownerMapping = joinTbl.getOwnerMapping();
        SQLExpression ownerExpr = exprFactory.newExpression(subStmt, joinSqlTbl, ownerMapping);
        SQLExpression ownerIdExpr =
            exprFactory.newExpression(
                stmt, mapExpr.getSQLTable(), mapExpr.getSQLTable().getTable().getIdMapping());
        subStmt.whereAnd(ownerExpr.eq(ownerIdExpr), true);

        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr =
              exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valTbl.getIdMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valTbl.getIdMapping());
          subStmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      }
    } else if (mmd.getMap().getMapType() == MapType.MAP_TYPE_KEY_IN_VALUE) {
      // Key stored in value table
      DatastoreClass valTbl = storeMgr.getDatastoreClass(mmd.getMap().getValueType(), clr);
      JavaTypeMapping ownerMapping = null;
      if (mmd.getMappedBy() != null) {
        ownerMapping = valTbl.getMemberMapping(valCmd.getMetaDataForMember(mmd.getMappedBy()));
      } else {
        ownerMapping = valTbl.getExternalMapping(mmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
      }

      subStmt = new SQLStatement(stmt, storeMgr, valTbl, null, null);
      subStmt.setClassLoaderResolver(clr);
      JavaTypeMapping oneMapping = storeMgr.getMappingManager().getMapping(Integer.class);
      subStmt.select(exprFactory.newLiteral(subStmt, oneMapping, 1), null);

      // Restrict to map owner (on value table)
      SQLExpression ownerExpr =
          exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), ownerMapping);
      SQLExpression ownerIdExpr =
          exprFactory.newExpression(
              stmt, mapExpr.getSQLTable(), mapExpr.getSQLTable().getTable().getIdMapping());
      subStmt.whereAnd(ownerExpr.eq(ownerIdExpr), true);

      if (valIsUnbound) {
        // Bind the variable in the QueryGenerator
        valExpr =
            exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valTbl.getIdMapping());
        stmt.getQueryGenerator()
            .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
      } else {
        // Add restrict to value
        JavaTypeMapping valMapping = valTbl.getIdMapping();
        SQLExpression valIdExpr =
            exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valMapping);
        subStmt.whereAnd(valIdExpr.eq(valExpr), true);
      }
    } else if (mmd.getMap().getMapType() == MapType.MAP_TYPE_VALUE_IN_KEY) {
      AbstractClassMetaData keyCmd = mmd.getMap().getKeyClassMetaData(clr, mmgr);
      DatastoreClass keyTbl = storeMgr.getDatastoreClass(mmd.getMap().getKeyType(), clr);
      JavaTypeMapping ownerMapping = null;
      if (mmd.getMappedBy() != null) {
        ownerMapping = keyTbl.getMemberMapping(keyCmd.getMetaDataForMember(mmd.getMappedBy()));
      } else {
        ownerMapping = keyTbl.getExternalMapping(mmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
      }

      AbstractMemberMetaData keyValMmd =
          keyCmd.getMetaDataForMember(mmd.getValueMetaData().getMappedBy());
      if (valCmd == null) {
        subStmt = new SQLStatement(stmt, storeMgr, keyTbl, null, null);
        subStmt.setClassLoaderResolver(clr);
        JavaTypeMapping oneMapping = storeMgr.getMappingManager().getMapping(Integer.class);
        subStmt.select(exprFactory.newLiteral(subStmt, oneMapping, 1), null);

        // Restrict to map owner (on key table)
        SQLExpression ownerExpr =
            exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), ownerMapping);
        SQLExpression ownerIdExpr =
            exprFactory.newExpression(
                stmt, mapExpr.getSQLTable(), mapExpr.getSQLTable().getTable().getIdMapping());
        subStmt.whereAnd(ownerExpr.eq(ownerIdExpr), true);

        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr =
              exprFactory.newExpression(
                  subStmt, subStmt.getPrimaryTable(), keyTbl.getMemberMapping(keyValMmd));
          stmt.getQueryGenerator()
              .bindVariable(varName, null, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          JavaTypeMapping valMapping = keyTbl.getMemberMapping(keyValMmd);
          SQLExpression valIdExpr =
              exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valMapping);
          subStmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      } else {
        DatastoreClass valTbl = storeMgr.getDatastoreClass(mmd.getMap().getValueType(), clr);
        subStmt = new SQLStatement(stmt, storeMgr, valTbl, null, null);
        subStmt.setClassLoaderResolver(clr);
        JavaTypeMapping oneMapping = storeMgr.getMappingManager().getMapping(Integer.class);
        subStmt.select(exprFactory.newLiteral(subStmt, oneMapping, 1), null);

        // Join to key table
        SQLTable keySqlTbl =
            subStmt.innerJoin(
                subStmt.getPrimaryTable(),
                valTbl.getIdMapping(),
                keyTbl,
                null,
                keyTbl.getMemberMapping(keyValMmd),
                null,
                null);

        // Restrict to map owner (on key table)
        SQLExpression ownerExpr = exprFactory.newExpression(subStmt, keySqlTbl, ownerMapping);
        SQLExpression ownerIdExpr =
            exprFactory.newExpression(
                stmt, mapExpr.getSQLTable(), mapExpr.getSQLTable().getTable().getIdMapping());
        subStmt.whereAnd(ownerExpr.eq(ownerIdExpr), true);

        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr =
              exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valTbl.getIdMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(subStmt, subStmt.getPrimaryTable(), valTbl.getIdMapping());
          subStmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      }
    }

    return new BooleanSubqueryExpression(stmt, "EXISTS", subStmt);
  }
  /**
   * Method to return an expression for Map.containsValue using INNER JOIN to the element. This is
   * only for use when there are no "!containsValue" and no "OR" operations. Creates SQL by adding
   * INNER JOIN to the join table (where it exists), and also to the value table adding an AND
   * condition on the value (with value of the valueExpr). Returns a BooleanExpression "TRUE" (since
   * the INNER JOIN will guarantee if the value is contained of not).
   *
   * @param mapExpr Map expression
   * @param valExpr Expression for the value
   * @return Contains expression
   */
  protected SQLExpression containsAsInnerJoin(MapExpression mapExpr, SQLExpression valExpr) {
    boolean valIsUnbound = (valExpr instanceof UnboundExpression);
    String varName = null;
    String valAlias = null;
    if (valIsUnbound) {
      varName = ((UnboundExpression) valExpr).getVariableName();
      NucleusLogger.QUERY.debug(
          "map.containsValue("
              + valExpr
              + ") binding unbound variable "
              + varName
              + " using INNER JOIN");
      // TODO What if the variable is declared as a subtype, handle this see
      // CollectionContainsMethod
    } else if (!stmt.getQueryGenerator().hasExplicitJoins()) {
      JoinType joinType = stmt.getJoinTypeForTable(valExpr.getSQLTable());
      if (joinType == JoinType.CROSS_JOIN) {
        // Value is currently joined via CROSS JOIN, so remove it (and use INNER JOIN below)
        valAlias = stmt.removeCrossJoin(valExpr.getSQLTable());
        valIsUnbound = true;
        NucleusLogger.QUERY.debug(
            "map.containsValue("
                + valExpr
                + ") was previously bound as CROSS JOIN but changing to INNER JOIN");
      }

      // TODO If owner is joined via CROSS JOIN and value is already present then remove CROSS JOIN
      // and join via INNER JOIN
    }

    RDBMSStoreManager storeMgr = stmt.getRDBMSManager();
    MetaDataManager mmgr = storeMgr.getMetaDataManager();
    AbstractMemberMetaData mmd = mapExpr.getJavaTypeMapping().getMemberMetaData();
    AbstractClassMetaData valCmd = mmd.getMap().getValueClassMetaData(clr, mmgr);
    if (mmd.getMap().getMapType() == MapType.MAP_TYPE_JOIN) {
      // Map formed in join table - add join to join table, then to value table (if present)
      MapTable mapTbl = (MapTable) storeMgr.getTable(mmd);
      SQLTable joinSqlTbl =
          stmt.innerJoin(
              mapExpr.getSQLTable(),
              mapExpr.getSQLTable().getTable().getIdMapping(),
              mapTbl,
              null,
              mapTbl.getOwnerMapping(),
              null,
              null);
      if (valCmd != null) {
        if (valIsUnbound) {
          DatastoreClass valTbl = storeMgr.getDatastoreClass(valCmd.getFullClassName(), clr);
          SQLTable valSqlTbl =
              stmt.innerJoin(
                  joinSqlTbl,
                  mapTbl.getValueMapping(),
                  valTbl,
                  valAlias,
                  valTbl.getIdMapping(),
                  null,
                  null);

          // Bind the variable in the QueryGenerator
          valExpr = exprFactory.newExpression(stmt, valSqlTbl, valSqlTbl.getTable().getIdMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(stmt, joinSqlTbl, mapTbl.getValueMapping());
          stmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      } else {
        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr = exprFactory.newExpression(stmt, joinSqlTbl, mapTbl.getValueMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, null, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(stmt, joinSqlTbl, mapTbl.getValueMapping());
          stmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      }
    } else if (mmd.getMap().getMapType() == MapType.MAP_TYPE_KEY_IN_VALUE) {
      // Map formed in value table - add join to value table
      DatastoreClass valTbl = storeMgr.getDatastoreClass(valCmd.getFullClassName(), clr);
      JavaTypeMapping ownerMapping = null;
      if (mmd.getMappedBy() != null) {
        ownerMapping = valTbl.getMemberMapping(valCmd.getMetaDataForMember(mmd.getMappedBy()));
      } else {
        ownerMapping = valTbl.getExternalMapping(mmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
      }
      SQLTable valSqlTbl =
          stmt.innerJoin(
              mapExpr.getSQLTable(),
              mapExpr.getSQLTable().getTable().getIdMapping(),
              valTbl,
              valAlias,
              ownerMapping,
              null,
              null);

      if (valIsUnbound) {
        // Bind the variable in the QueryGenerator
        valExpr = exprFactory.newExpression(stmt, valSqlTbl, valTbl.getIdMapping());
        stmt.getQueryGenerator()
            .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
      } else {
        // Add restrict to value
        SQLExpression valIdExpr = exprFactory.newExpression(stmt, valSqlTbl, valTbl.getIdMapping());
        stmt.whereAnd(valIdExpr.eq(valExpr), true);
      }
    } else if (mmd.getMap().getMapType() == MapType.MAP_TYPE_VALUE_IN_KEY) {
      // Map formed in key table - add join to key table then to value table
      AbstractClassMetaData keyCmd = mmd.getMap().getKeyClassMetaData(clr, mmgr);
      DatastoreClass keyTbl = storeMgr.getDatastoreClass(keyCmd.getFullClassName(), clr);
      AbstractMemberMetaData keyValMmd =
          keyCmd.getMetaDataForMember(mmd.getValueMetaData().getMappedBy());
      JavaTypeMapping ownerMapping = null;
      if (mmd.getMappedBy() != null) {
        ownerMapping = keyTbl.getMemberMapping(keyCmd.getMetaDataForMember(mmd.getMappedBy()));
      } else {
        ownerMapping = keyTbl.getExternalMapping(mmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
      }
      SQLTable keySqlTbl =
          stmt.innerJoin(
              mapExpr.getSQLTable(),
              mapExpr.getSQLTable().getTable().getIdMapping(),
              keyTbl,
              null,
              ownerMapping,
              null,
              null);

      if (valCmd != null) {
        DatastoreClass valTbl = storeMgr.getDatastoreClass(valCmd.getFullClassName(), clr);
        SQLTable valSqlTbl =
            stmt.innerJoin(
                keySqlTbl,
                keyTbl.getMemberMapping(keyValMmd),
                valTbl,
                valAlias,
                valTbl.getIdMapping(),
                null,
                null);

        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr = exprFactory.newExpression(stmt, valSqlTbl, valTbl.getIdMapping());
          stmt.getQueryGenerator()
              .bindVariable(varName, valCmd, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(stmt, valSqlTbl, valTbl.getIdMapping());
          stmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      } else {
        if (valIsUnbound) {
          // Bind the variable in the QueryGenerator
          valExpr = exprFactory.newExpression(stmt, keySqlTbl, keyTbl.getMemberMapping(keyValMmd));
          stmt.getQueryGenerator()
              .bindVariable(varName, null, valExpr.getSQLTable(), valExpr.getJavaTypeMapping());
        } else {
          // Add restrict to value
          SQLExpression valIdExpr =
              exprFactory.newExpression(stmt, keySqlTbl, keyTbl.getMemberMapping(keyValMmd));
          stmt.whereAnd(valIdExpr.eq(valExpr), true);
        }
      }
    }

    JavaTypeMapping m = exprFactory.getMappingForType(boolean.class, true);
    return exprFactory.newLiteral(stmt, m, true).eq(exprFactory.newLiteral(stmt, m, true));
  }