|
@@ -0,0 +1,353 @@
|
|
|
+/*
|
|
|
+ * Licensed to Elasticsearch under one or more contributor
|
|
|
+ * license agreements. See the NOTICE file distributed with
|
|
|
+ * this work for additional information regarding copyright
|
|
|
+ * ownership. Elasticsearch 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 org.elasticsearch.join.mapper;
|
|
|
+
|
|
|
+import org.elasticsearch.common.compress.CompressedXContent;
|
|
|
+import org.elasticsearch.common.xcontent.XContentFactory;
|
|
|
+import org.elasticsearch.common.xcontent.XContentType;
|
|
|
+import org.elasticsearch.index.IndexService;
|
|
|
+import org.elasticsearch.index.mapper.DocumentMapper;
|
|
|
+import org.elasticsearch.index.mapper.MapperException;
|
|
|
+import org.elasticsearch.index.mapper.MapperParsingException;
|
|
|
+import org.elasticsearch.index.mapper.MapperService;
|
|
|
+import org.elasticsearch.index.mapper.ParsedDocument;
|
|
|
+import org.elasticsearch.index.mapper.SourceToParse;
|
|
|
+import org.elasticsearch.join.ParentJoinPlugin;
|
|
|
+import org.elasticsearch.plugins.Plugin;
|
|
|
+import org.elasticsearch.test.ESSingleNodeTestCase;
|
|
|
+
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Collections;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.containsString;
|
|
|
+
|
|
|
+public class ParentJoinFieldMapperTests extends ESSingleNodeTestCase {
|
|
|
+ @Override
|
|
|
+ protected Collection<Class<? extends Plugin>> getPlugins() {
|
|
|
+ return Collections.singletonList(ParentJoinPlugin.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testSingleLevel() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject().string();
|
|
|
+ DocumentMapper docMapper = createIndex("test")
|
|
|
+ .mapperService().documentMapperParser().parse("type", new CompressedXContent(mapping));
|
|
|
+
|
|
|
+ // Doc without join
|
|
|
+ ParsedDocument doc = docMapper.parse(SourceToParse.source("test", "type", "0",
|
|
|
+ XContentFactory.jsonBuilder().startObject().endObject().bytes(), XContentType.JSON));
|
|
|
+ assertNull(doc.rootDoc().getBinaryValue("join_field"));
|
|
|
+
|
|
|
+ // Doc parent
|
|
|
+ doc = docMapper.parse(SourceToParse.source("test", "type", "1",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .field("join_field", "parent")
|
|
|
+ .endObject().bytes(), XContentType.JSON));
|
|
|
+ assertEquals("1", doc.rootDoc().getBinaryValue("join_field#parent").utf8ToString());
|
|
|
+ assertEquals("parent", doc.rootDoc().getBinaryValue("join_field").utf8ToString());
|
|
|
+
|
|
|
+ // Doc child
|
|
|
+ doc = docMapper.parse(SourceToParse.source("test", "type", "2",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("name", "child")
|
|
|
+ .field("parent", "1")
|
|
|
+ .endObject()
|
|
|
+ .endObject().bytes(), XContentType.JSON).routing("1"));
|
|
|
+ assertEquals("1", doc.rootDoc().getBinaryValue("join_field#parent").utf8ToString());
|
|
|
+ assertEquals("child", doc.rootDoc().getBinaryValue("join_field").utf8ToString());
|
|
|
+
|
|
|
+ // Unkwnown join name
|
|
|
+ MapperException exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> docMapper.parse(SourceToParse.source("test", "type", "1",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .field("join_field", "unknown")
|
|
|
+ .endObject().bytes(), XContentType.JSON)));
|
|
|
+ assertThat(exc.getRootCause().getMessage(), containsString("unknown join name [unknown] for field [join_field]"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testMultipleLevels() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .field("child", "grand_child")
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject().string();
|
|
|
+ DocumentMapper docMapper = createIndex("test").mapperService()
|
|
|
+ .documentMapperParser().parse("type", new CompressedXContent(mapping));
|
|
|
+
|
|
|
+ // Doc without join
|
|
|
+ ParsedDocument doc = docMapper.parse(SourceToParse.source("test", "type", "0",
|
|
|
+ XContentFactory.jsonBuilder().startObject().endObject().bytes(), XContentType.JSON));
|
|
|
+ assertNull(doc.rootDoc().getBinaryValue("join_field"));
|
|
|
+
|
|
|
+ // Doc parent
|
|
|
+ doc = docMapper.parse(SourceToParse.source("test", "type", "1",
|
|
|
+ XContentFactory.jsonBuilder()
|
|
|
+ .startObject()
|
|
|
+ .field("join_field", "parent")
|
|
|
+ .endObject().bytes(), XContentType.JSON));
|
|
|
+ assertEquals("1", doc.rootDoc().getBinaryValue("join_field#parent").utf8ToString());
|
|
|
+ assertEquals("parent", doc.rootDoc().getBinaryValue("join_field").utf8ToString());
|
|
|
+
|
|
|
+ // Doc child
|
|
|
+ doc = docMapper.parse(SourceToParse.source("test", "type", "2",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("name", "child")
|
|
|
+ .field("parent", "1")
|
|
|
+ .endObject()
|
|
|
+ .endObject().bytes(), XContentType.JSON).routing("1"));
|
|
|
+ assertEquals("1", doc.rootDoc().getBinaryValue("join_field#parent").utf8ToString());
|
|
|
+ assertEquals("2", doc.rootDoc().getBinaryValue("join_field#child").utf8ToString());
|
|
|
+ assertEquals("child", doc.rootDoc().getBinaryValue("join_field").utf8ToString());
|
|
|
+
|
|
|
+ // Doc child missing parent
|
|
|
+ MapperException exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> docMapper.parse(SourceToParse.source("test", "type", "2",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .field("join_field", "child")
|
|
|
+ .endObject().bytes(), XContentType.JSON).routing("1")));
|
|
|
+ assertThat(exc.getRootCause().getMessage(), containsString("[parent] is missing for join field [join_field]"));
|
|
|
+
|
|
|
+ // Doc child missing routing
|
|
|
+ exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> docMapper.parse(SourceToParse.source("test", "type", "2",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("name", "child")
|
|
|
+ .field("parent", "1")
|
|
|
+ .endObject()
|
|
|
+ .endObject().bytes(), XContentType.JSON)));
|
|
|
+ assertThat(exc.getRootCause().getMessage(), containsString("[routing] is missing for join field [join_field]"));
|
|
|
+
|
|
|
+ // Doc grand_child
|
|
|
+ doc = docMapper.parse(SourceToParse.source("test", "type", "3",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("name", "grand_child")
|
|
|
+ .field("parent", "2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().bytes(), XContentType.JSON).routing("1"));
|
|
|
+ assertEquals("2", doc.rootDoc().getBinaryValue("join_field#child").utf8ToString());
|
|
|
+ assertEquals("grand_child", doc.rootDoc().getBinaryValue("join_field").utf8ToString());
|
|
|
+
|
|
|
+ // Unkwnown join name
|
|
|
+ exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> docMapper.parse(SourceToParse.source("test", "type", "1",
|
|
|
+ XContentFactory.jsonBuilder().startObject()
|
|
|
+ .field("join_field", "unknown")
|
|
|
+ .endObject().bytes(), XContentType.JSON)));
|
|
|
+ assertThat(exc.getRootCause().getMessage(), containsString("unknown join name [unknown] for field [join_field]"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testUpdateRelations() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IndexService indexService = createIndex("test");
|
|
|
+ indexService.mapperService().merge("type", new CompressedXContent(mapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false);
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IllegalStateException exc = expectThrows(IllegalStateException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getMessage(), containsString("cannot remove parent [parent] in join field [join_field]"));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .field("child", "grand_child1")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IllegalStateException exc = expectThrows(IllegalStateException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getMessage(), containsString("cannot remove child [grand_child2] in join field [join_field]"));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("uber_parent", "parent")
|
|
|
+ .field("parent", "child")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IllegalStateException exc = expectThrows(IllegalStateException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getMessage(), containsString("cannot create child [parent] from an existing parent"));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .field("grand_child2", "grand_grand_child")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IllegalStateException exc = expectThrows(IllegalStateException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getMessage(), containsString("cannot create parent [grand_child2] from an existing child]"));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .array("parent", "child", "child2")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, true);
|
|
|
+ ParentJoinFieldMapper mapper = (ParentJoinFieldMapper) indexService.mapperService()
|
|
|
+ .docMappers(false).iterator().next().mappers().getMapper("join_field");
|
|
|
+ assertTrue(mapper.hasChild("child2"));
|
|
|
+ assertFalse(mapper.hasParent("child2"));
|
|
|
+ assertTrue(mapper.hasChild("grand_child2"));
|
|
|
+ assertFalse(mapper.hasParent("grand_child2"));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ final String updateMapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .array("parent", "child", "child2")
|
|
|
+ .array("child", "grand_child1", "grand_child2")
|
|
|
+ .array("other", "child_other1", "child_other2")
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ indexService.mapperService().merge("type", new CompressedXContent(updateMapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, true);
|
|
|
+ ParentJoinFieldMapper mapper = (ParentJoinFieldMapper) indexService.mapperService()
|
|
|
+ .docMappers(false).iterator().next().mappers().getMapper("join_field");
|
|
|
+ assertTrue(mapper.hasParent("other"));
|
|
|
+ assertFalse(mapper.hasChild("other"));
|
|
|
+ assertTrue(mapper.hasChild("child_other1"));
|
|
|
+ assertFalse(mapper.hasParent("child_other1"));
|
|
|
+ assertTrue(mapper.hasChild("child_other2"));
|
|
|
+ assertFalse(mapper.hasParent("child_other2"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testInvalidJoinFieldInsideObject() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("object")
|
|
|
+ .startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IndexService indexService = createIndex("test");
|
|
|
+ MapperParsingException exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(mapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getRootCause().getMessage(),
|
|
|
+ containsString("join field [object.join_field] cannot be added inside an object or in a multi-field"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testInvalidJoinFieldInsideMultiFields() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("properties")
|
|
|
+ .startObject("number")
|
|
|
+ .field("type", "integer")
|
|
|
+ .startObject("fields")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject().endObject().string();
|
|
|
+ IndexService indexService = createIndex("test");
|
|
|
+ MapperParsingException exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> indexService.mapperService().merge("type", new CompressedXContent(mapping),
|
|
|
+ MapperService.MergeReason.MAPPING_UPDATE, false));
|
|
|
+ assertThat(exc.getRootCause().getMessage(),
|
|
|
+ containsString("join field [number.join_field] cannot be added inside an object or in a multi-field"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testMultipleJoinFields() throws Exception {
|
|
|
+ String mapping = XContentFactory.jsonBuilder().startObject()
|
|
|
+ .startObject("properties")
|
|
|
+ .startObject("join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("parent", "child")
|
|
|
+ .field("child", "grand_child")
|
|
|
+ .endObject()
|
|
|
+ .startObject("another_join_field")
|
|
|
+ .field("type", "join")
|
|
|
+ .field("product", "item")
|
|
|
+ .endObject()
|
|
|
+ .endObject()
|
|
|
+ .endObject().string();
|
|
|
+ DocumentMapper docMapper = createIndex("test").mapperService()
|
|
|
+ .documentMapperParser().parse("type", new CompressedXContent(mapping));
|
|
|
+
|
|
|
+ // Doc without join
|
|
|
+ ParsedDocument doc = docMapper.parse(SourceToParse.source("test", "type", "0",
|
|
|
+ XContentFactory.jsonBuilder().startObject().endObject().bytes(), XContentType.JSON));
|
|
|
+ assertNull(doc.rootDoc().getBinaryValue("join_field"));
|
|
|
+
|
|
|
+ // Doc parent
|
|
|
+ MapperParsingException exc = expectThrows(MapperParsingException.class,
|
|
|
+ () -> docMapper.parse(SourceToParse.source("test", "type", "1",
|
|
|
+ XContentFactory.jsonBuilder()
|
|
|
+ .startObject()
|
|
|
+ .field("join_field", "parent")
|
|
|
+ .startObject("another_join_field")
|
|
|
+ .field("name", "item")
|
|
|
+ .field("parent", "0")
|
|
|
+ .endObject()
|
|
|
+ .endObject().bytes(), XContentType.JSON)));
|
|
|
+ assertThat(exc.getRootCause().getMessage(), containsString("cannot have two join fields in the same document"));
|
|
|
+ }
|
|
|
+}
|