@Override
  protected void doMerge(Mapper mergeWith, boolean updateAllTypes) {
    super.doMerge(mergeWith, updateAllTypes);
    ParentFieldMapper fieldMergeWith = (ParentFieldMapper) mergeWith;
    if (Objects.equals(parentType, fieldMergeWith.parentType) == false) {
      throw new IllegalArgumentException(
          "The _parent field's type option can't be changed: ["
              + parentType
              + "]->["
              + fieldMergeWith.parentType
              + "]");
    }

    List<String> conflicts = new ArrayList<>();
    fieldType()
        .checkCompatibility(
            fieldMergeWith.fieldType(), conflicts, true); // always strict, this cannot change
    parentJoinFieldType.checkCompatibility(
        fieldMergeWith.parentJoinFieldType, conflicts, true); // same here
    if (childJoinFieldType != null) {
      // TODO: this can be set to false when the old parent/child impl is removed, we can do eager
      // global ordinals loading per type.
      childJoinFieldType.checkCompatibility(
          fieldMergeWith.childJoinFieldType, conflicts, updateAllTypes == false);
    }
    if (conflicts.isEmpty() == false) {
      throw new IllegalArgumentException("Merge conflicts: " + conflicts);
    }

    if (active()) {
      childJoinFieldType = fieldMergeWith.childJoinFieldType.clone();
    }
  }
 @Test
 public void testBasicQuerySanities() {
   Query childQuery = new TermQuery(new Term("field", "value"));
   ParentFieldMapper parentFieldMapper =
       SearchContext.current().mapperService().documentMapper("child").parentFieldMapper();
   ParentChildIndexFieldData parentChildIndexFieldData =
       SearchContext.current().fieldData().getForField(parentFieldMapper.fieldType());
   BitDocIdSetFilter parentFilter =
       wrapWithBitSetFilter(
           new QueryWrapperFilter(new TermQuery(new Term(TypeFieldMapper.NAME, "parent"))));
   Query query =
       new ChildrenConstantScoreQuery(
           parentChildIndexFieldData,
           childQuery,
           "parent",
           "child",
           parentFilter,
           12,
           wrapWithBitSetFilter(Queries.newNonNestedFilter()));
   QueryUtils.check(query);
 }
  @Override
  public AggregatorFactory parse(
      String aggregationName, XContentParser parser, SearchContext context) throws IOException {
    String childType = null;

    XContentParser.Token token;
    String currentFieldName = null;
    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
      if (token == XContentParser.Token.FIELD_NAME) {
        currentFieldName = parser.currentName();
      } else if (token == XContentParser.Token.VALUE_STRING) {
        if ("type".equals(currentFieldName)) {
          childType = parser.text();
        } else {
          throw new SearchParseException(
              context,
              "Unknown key for a "
                  + token
                  + " in ["
                  + aggregationName
                  + "]: ["
                  + currentFieldName
                  + "].");
        }
      } else {
        throw new SearchParseException(
            context, "Unexpected token " + token + " in [" + aggregationName + "].");
      }
    }

    if (childType == null) {
      throw new SearchParseException(
          context, "Missing [child_type] field for children aggregation [" + aggregationName + "]");
    }

    DocumentMapper childDocMapper = context.mapperService().documentMapper(childType);
    if (childDocMapper == null) {
      throw new SearchParseException(
          context, "[children] No mapping for for type [" + childType + "]");
    }
    ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper();
    if (!parentFieldMapper.active()) {
      throw new SearchParseException(context, "[children] _parent field not configured");
    }

    String parentType = parentFieldMapper.type();
    DocumentMapper parentDocMapper = context.mapperService().documentMapper(parentType);
    if (parentDocMapper == null) {
      throw new SearchParseException(
          context,
          "[children]  Type ["
              + childType
              + "] points to a non existent parent type ["
              + parentType
              + "]");
    }

    Filter parentFilter = context.filterCache().cache(parentDocMapper.typeFilter());
    Filter childFilter = context.filterCache().cache(childDocMapper.typeFilter());

    ParentChildIndexFieldData parentChildIndexFieldData =
        context.fieldData().getForField(parentFieldMapper);
    ValuesSourceConfig<ValuesSource.Bytes.WithOrdinals.ParentChild> config =
        new ValuesSourceConfig<>(ValuesSource.Bytes.WithOrdinals.ParentChild.class);
    config.fieldContext(
        new FieldContext(
            parentFieldMapper.names().indexName(), parentChildIndexFieldData, parentFieldMapper));
    return new ParentToChildrenAggregator.Factory(
        aggregationName, config, parentType, parentFilter, childFilter);
  }
  @Override
  public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
    ensureNotDeleteByQuery(NAME, parseContext);
    XContentParser parser = parseContext.parser();

    boolean queryFound = false;
    float boost = 1.0f;
    String childType = null;
    ScoreType scoreType = ScoreType.NONE;
    int minChildren = 0;
    int maxChildren = 0;
    int shortCircuitParentDocSet = 8192;
    String queryName = null;
    Tuple<String, SubSearchContext> innerHits = null;

    String currentFieldName = null;
    XContentParser.Token token;
    XContentStructure.InnerQuery iq = null;
    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
      if (token == XContentParser.Token.FIELD_NAME) {
        currentFieldName = parser.currentName();
      } else if (token == XContentParser.Token.START_OBJECT) {
        // Usually, the query would be parsed here, but the child
        // type may not have been extracted yet, so use the
        // XContentStructure.<type> facade to parse if available,
        // or delay parsing if not.
        if ("query".equals(currentFieldName)) {
          iq =
              new XContentStructure.InnerQuery(
                  parseContext, childType == null ? null : new String[] {childType});
          queryFound = true;
        } else if ("inner_hits".equals(currentFieldName)) {
          innerHits = innerHitsQueryParserHelper.parse(parseContext);
        } else {
          throw new QueryParsingException(
              parseContext.index(),
              "[has_child] query does not support [" + currentFieldName + "]");
        }
      } else if (token.isValue()) {
        if ("type".equals(currentFieldName)
            || "child_type".equals(currentFieldName)
            || "childType".equals(currentFieldName)) {
          childType = parser.text();
        } else if ("score_type".equals(currentFieldName) || "scoreType".equals(currentFieldName)) {
          scoreType = ScoreType.fromString(parser.text());
        } else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
          scoreType = ScoreType.fromString(parser.text());
        } else if ("boost".equals(currentFieldName)) {
          boost = parser.floatValue();
        } else if ("min_children".equals(currentFieldName)
            || "minChildren".equals(currentFieldName)) {
          minChildren = parser.intValue(true);
        } else if ("max_children".equals(currentFieldName)
            || "maxChildren".equals(currentFieldName)) {
          maxChildren = parser.intValue(true);
        } else if ("short_circuit_cutoff".equals(currentFieldName)) {
          shortCircuitParentDocSet = parser.intValue();
        } else if ("_name".equals(currentFieldName)) {
          queryName = parser.text();
        } else {
          throw new QueryParsingException(
              parseContext.index(),
              "[has_child] query does not support [" + currentFieldName + "]");
        }
      }
    }
    if (!queryFound) {
      throw new QueryParsingException(parseContext.index(), "[has_child] requires 'query' field");
    }
    if (childType == null) {
      throw new QueryParsingException(parseContext.index(), "[has_child] requires 'type' field");
    }

    Query innerQuery = iq.asQuery(childType);

    if (innerQuery == null) {
      return null;
    }
    innerQuery.setBoost(boost);

    DocumentMapper childDocMapper = parseContext.mapperService().documentMapper(childType);
    if (childDocMapper == null) {
      throw new QueryParsingException(
          parseContext.index(), "[has_child] No mapping for for type [" + childType + "]");
    }
    if (!childDocMapper.parentFieldMapper().active()) {
      throw new QueryParsingException(
          parseContext.index(),
          "[has_child]  Type [" + childType + "] does not have parent mapping");
    }

    if (innerHits != null) {
      InnerHitsContext.ParentChildInnerHits parentChildInnerHits =
          new InnerHitsContext.ParentChildInnerHits(
              innerHits.v2(), innerQuery, null, childDocMapper);
      String name = innerHits.v1() != null ? innerHits.v1() : childType;
      parseContext.addInnerHits(name, parentChildInnerHits);
    }

    ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper();
    if (!parentFieldMapper.active()) {
      throw new QueryParsingException(
          parseContext.index(), "[has_child] _parent field not configured");
    }

    String parentType = parentFieldMapper.type();
    DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType);
    if (parentDocMapper == null) {
      throw new QueryParsingException(
          parseContext.index(),
          "[has_child]  Type ["
              + childType
              + "] points to a non existent parent type ["
              + parentType
              + "]");
    }

    if (maxChildren > 0 && maxChildren < minChildren) {
      throw new QueryParsingException(
          parseContext.index(), "[has_child] 'max_children' is less than 'min_children'");
    }

    BitDocIdSetFilter nonNestedDocsFilter = null;
    if (parentDocMapper.hasNestedObjects()) {
      nonNestedDocsFilter = parseContext.bitsetFilter(NonNestedDocsFilter.INSTANCE);
    }

    // wrap the query with type query
    innerQuery =
        new FilteredQuery(
            innerQuery,
            parseContext.cacheFilter(
                childDocMapper.typeFilter(), null, parseContext.autoFilterCachePolicy()));

    Query query;
    Filter parentFilter =
        parseContext.cacheFilter(
            parentDocMapper.typeFilter(), null, parseContext.autoFilterCachePolicy());
    ParentChildIndexFieldData parentChildIndexFieldData =
        parseContext.getForField(parentFieldMapper);
    if (minChildren > 1 || maxChildren > 0 || scoreType != ScoreType.NONE) {
      query =
          new ChildrenQuery(
              parentChildIndexFieldData,
              parentType,
              childType,
              parentFilter,
              innerQuery,
              scoreType,
              minChildren,
              maxChildren,
              shortCircuitParentDocSet,
              nonNestedDocsFilter);
    } else {
      query =
          new ChildrenConstantScoreQuery(
              parentChildIndexFieldData,
              innerQuery,
              parentType,
              childType,
              parentFilter,
              shortCircuitParentDocSet,
              nonNestedDocsFilter);
    }
    if (queryName != null) {
      parseContext.addNamedFilter(queryName, new CustomQueryWrappingFilter(query));
    }
    query.setBoost(boost);
    return query;
  }
  @Override
  public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
    XContentParser parser = parseContext.parser();

    boolean queryFound = false;
    float boost = 1.0f;
    String parentType = null;
    boolean score = false;
    String queryName = null;

    String currentFieldName = null;
    XContentParser.Token token;
    XContentStructure.InnerQuery iq = null;
    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
      if (token == XContentParser.Token.FIELD_NAME) {
        currentFieldName = parser.currentName();
      } else if (token == XContentParser.Token.START_OBJECT) {
        // Usually, the query would be parsed here, but the child
        // type may not have been extracted yet, so use the
        // XContentStructure.<type> facade to parse if available,
        // or delay parsing if not.
        if ("query".equals(currentFieldName)) {
          iq =
              new XContentStructure.InnerQuery(
                  parseContext, parentType == null ? null : new String[] {parentType});
          queryFound = true;
        } else {
          throw new QueryParsingException(
              parseContext.index(),
              "[has_parent] query does not support [" + currentFieldName + "]");
        }
      } else if (token.isValue()) {
        if ("type".equals(currentFieldName)
            || "parent_type".equals(currentFieldName)
            || "parentType".equals(currentFieldName)) {
          parentType = parser.text();
        } else if ("_scope".equals(currentFieldName)) {
          throw new QueryParsingException(
              parseContext.index(),
              "the [_scope] support in [has_parent] query has been removed, use a filter as a facet_filter in the relevant global facet");
        } else if ("score_type".equals(currentFieldName) || "scoreType".equals(currentFieldName)) {
          String scoreTypeValue = parser.text();
          if ("score".equals(scoreTypeValue)) {
            score = true;
          } else if ("none".equals(scoreTypeValue)) {
            score = false;
          }
        } else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
          String scoreModeValue = parser.text();
          if ("score".equals(scoreModeValue)) {
            score = true;
          } else if ("none".equals(scoreModeValue)) {
            score = false;
          }
        } else if ("boost".equals(currentFieldName)) {
          boost = parser.floatValue();
        } else if ("_name".equals(currentFieldName)) {
          queryName = parser.text();
        } else {
          throw new QueryParsingException(
              parseContext.index(),
              "[has_parent] query does not support [" + currentFieldName + "]");
        }
      }
    }
    if (!queryFound) {
      throw new QueryParsingException(
          parseContext.index(), "[has_parent] query requires 'query' field");
    }
    if (parentType == null) {
      throw new QueryParsingException(
          parseContext.index(), "[has_parent] query requires 'parent_type' field");
    }

    Query innerQuery = iq.asQuery(parentType);

    if (innerQuery == null) {
      return null;
    }

    DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType);
    if (parentDocMapper == null) {
      throw new QueryParsingException(
          parseContext.index(),
          "[has_parent] query configured 'parent_type' [" + parentType + "] is not a valid type");
    }

    innerQuery.setBoost(boost);
    // wrap the query with type query
    innerQuery =
        new XFilteredQuery(
            innerQuery, parseContext.cacheFilter(parentDocMapper.typeFilter(), null));

    ParentChildIndexFieldData parentChildIndexFieldData = null;
    Set<String> parentTypes = new HashSet<>(5);
    parentTypes.add(parentType);
    for (DocumentMapper documentMapper : parseContext.mapperService()) {
      ParentFieldMapper parentFieldMapper = documentMapper.parentFieldMapper();
      if (parentFieldMapper.active()) {
        parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper);
        DocumentMapper parentTypeDocumentMapper =
            parseContext.mapperService().documentMapper(parentFieldMapper.type());
        if (parentTypeDocumentMapper == null) {
          // Only add this, if this parentFieldMapper (also a parent)  isn't a child of another
          // parent.
          parentTypes.add(parentFieldMapper.type());
        }
      }
    }
    if (parentChildIndexFieldData == null) {
      throw new QueryParsingException(
          parseContext.index(), "[has_parent] no _parent field configured");
    }

    Filter parentFilter;
    if (parentTypes.size() == 1) {
      DocumentMapper documentMapper =
          parseContext.mapperService().documentMapper(parentTypes.iterator().next());
      parentFilter = parseContext.cacheFilter(documentMapper.typeFilter(), null);
    } else {
      XBooleanFilter parentsFilter = new XBooleanFilter();
      for (String parentTypeStr : parentTypes) {
        DocumentMapper documentMapper = parseContext.mapperService().documentMapper(parentTypeStr);
        Filter filter = parseContext.cacheFilter(documentMapper.typeFilter(), null);
        parentsFilter.add(filter, BooleanClause.Occur.SHOULD);
      }
      parentFilter = parentsFilter;
    }
    Filter childrenFilter = parseContext.cacheFilter(new NotFilter(parentFilter), null);

    boolean deleteByQuery = "delete_by_query".equals(SearchContext.current().source());
    Query query;
    if (!deleteByQuery && score) {
      query = new ParentQuery(parentChildIndexFieldData, innerQuery, parentType, childrenFilter);
    } else {
      query =
          new ParentConstantScoreQuery(
              parentChildIndexFieldData, innerQuery, parentType, childrenFilter);
      if (deleteByQuery) {
        query = new XConstantScoreQuery(new DeleteByQueryWrappingFilter(query));
      }
    }
    query.setBoost(boost);
    if (queryName != null) {
      parseContext.addNamedFilter(queryName, new CustomQueryWrappingFilter(query));
    }
    return query;
  }
  @Test
  public void testSimple() throws Exception {
    Directory directory = newDirectory();
    RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);

    for (int parent = 1; parent <= 5; parent++) {
      Document document = new Document();
      document.add(
          new StringField(
              UidFieldMapper.NAME,
              Uid.createUid("parent", Integer.toString(parent)),
              Field.Store.NO));
      document.add(new StringField(TypeFieldMapper.NAME, "parent", Field.Store.NO));
      indexWriter.addDocument(document);

      for (int child = 1; child <= 3; child++) {
        document = new Document();
        document.add(
            new StringField(
                UidFieldMapper.NAME,
                Uid.createUid("child", Integer.toString(parent * 3 + child)),
                Field.Store.NO));
        document.add(new StringField(TypeFieldMapper.NAME, "child", Field.Store.NO));
        document.add(
            new StringField(
                ParentFieldMapper.NAME,
                Uid.createUid("parent", Integer.toString(parent)),
                Field.Store.NO));
        document.add(new StringField("field1", "value" + child, Field.Store.NO));
        indexWriter.addDocument(document);
      }
    }

    IndexReader indexReader = DirectoryReader.open(indexWriter.w, false);
    IndexSearcher searcher = new IndexSearcher(indexReader);
    ((TestSearchContext) SearchContext.current())
        .setSearcher(
            new ContextIndexSearcher(
                SearchContext.current(),
                new Engine.Searcher(
                    ChildrenConstantScoreQueryTests.class.getSimpleName(), searcher)));

    TermQuery childQuery = new TermQuery(new Term("field1", "value" + (1 + random().nextInt(3))));
    BitDocIdSetFilter parentFilter =
        wrapWithBitSetFilter(
            new QueryWrapperFilter(new TermQuery(new Term(TypeFieldMapper.NAME, "parent"))));
    int shortCircuitParentDocSet = random().nextInt(5);
    ParentFieldMapper parentFieldMapper =
        SearchContext.current().mapperService().documentMapper("child").parentFieldMapper();
    ParentChildIndexFieldData parentChildIndexFieldData =
        SearchContext.current().fieldData().getForField(parentFieldMapper.fieldType());
    ChildrenConstantScoreQuery query =
        new ChildrenConstantScoreQuery(
            parentChildIndexFieldData,
            childQuery,
            "parent",
            "child",
            parentFilter,
            shortCircuitParentDocSet,
            null);

    BitSetCollector collector = new BitSetCollector(indexReader.maxDoc());
    searcher.search(query, collector);
    FixedBitSet actualResult = collector.getResult();

    assertThat(actualResult.cardinality(), equalTo(5));

    indexWriter.close();
    indexReader.close();
    directory.close();
  }