public Object visit(AbstractDataFilter filter, Object rawContext) {
    VisitationContext context = (VisitationContext) rawContext;

    if (DataValueBean.isLargeValue(filter.getOperand())) {
      throw new InternalException(
          "Inlined data filter evaluation is not supported for big data values.");
    }

    final boolean isPrefetchHint = filter instanceof DataPrefetchHint;
    final boolean isFilterUsedInAndTerm = isAndTerm(context);
    final boolean isIsNullFilter = isIsNullFilter(filter);

    // join data_value table at most once for every dataID involved with the query, this
    // join will eventually be reused by successive DataFilters targeting the same
    // dataID (especially needed for ORed predicate to prevent combinatorial explosion)

    Pair joinKey = new Pair(Integer.valueOf(filter.getFilterMode()), new DataAttributeKey(filter));
    Join dvJoin = (Join) dataJoinMapping.get(joinKey);

    // collect qualifying data OIDs
    Map<Long, IData> dataMap =
        findAllDataRtOids(filter.getDataID(), context.getEvaluationContext().getModelManager());
    DataFilterExtension dataFilterExtension = SpiUtils.createDataFilterExtension(dataMap);

    final DataFilterExtensionContext dataFilterExtensionContext =
        context.getDataFilterExtensionContext();
    if (null == dvJoin) {
      // first use of this specific dataID, setup join
      // a dummy queryDescriptor needed here
      QueryDescriptor queryDescriptor =
          QueryDescriptor.from(ProcessInstanceBean.class)
              .select(
                  ProcessInstanceBean.FIELD__OID,
                  ProcessInstanceBean.FIELD__SCOPE_PROCESS_INSTANCE);

      JoinFactory joinFactory = new JoinFactory(context);
      dvJoin =
          dataFilterExtension.createDvJoin(
              queryDescriptor,
              filter,
              dataJoinMapping.size() + 1,
              dataFilterExtensionContext,
              isFilterUsedInAndTerm,
              joinFactory);

      // use INNER JOIN if both is valid
      // * filter is used in an AND term
      // * filter is NOT prefetch hint
      // otherwise use LEFT OUTER JOIN
      dvJoin.setRequired(isFilterUsedInAndTerm && !isPrefetchHint && !isIsNullFilter);

      if (dataFilterExtensionContext.useDistinct()) {
        context.useDistinct(dataFilterExtensionContext.useDistinct());
      }
      dataJoinMapping.put(joinKey, dvJoin);

      AndTerm andTerm = new AndTerm();
      dataFilterExtension.appendDataIdTerm(andTerm, dataMap, dvJoin, filter);
      if (andTerm.getParts().size() != 0) {
        dvJoin.where(andTerm);
      }
    } else {
      // if join already exists
      // * and filter is used in an AND term
      // * and filter is NOT prefetch hint
      // then force join to be an INNER JOIN
      // otherwise leave it as it is
      if (isFilterUsedInAndTerm && !isPrefetchHint && !isIsNullFilter) {
        dvJoin.setRequired(true);
      }
    }

    if (isPrefetchHint) {
      final List<FieldRef> selectExtension = context.getSelectExtension();
      selectExtension.addAll(dataFilterExtension.getPrefetchSelectExtension(dvJoin));

      return NOTHING;
    } else {
      return dataFilterExtension.createPredicateTerm(
          dvJoin, filter, dataMap, dataFilterExtensionContext);
    }
  }