diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 046b741ba3593..9ed23f61bf0ea 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -449,19 +449,19 @@ public Builder add(FieldMapper.Builder builder) { return this; } - public Builder add(FieldMapper mapper) { + private void add(FieldMapper mapper) { mapperBuilders.put(mapper.simpleName(), context -> mapper); - return this; } - public Builder update(FieldMapper toMerge, MapperMergeContext context) { + private void update(FieldMapper toMerge, MapperMergeContext context) { if (mapperBuilders.containsKey(toMerge.simpleName()) == false) { - add(toMerge); + if (context.decrementFieldBudgetIfPossible(toMerge.mapperSize())) { + add(toMerge); + } } else { FieldMapper existing = mapperBuilders.get(toMerge.simpleName()).apply(context.getMapperBuilderContext()); add(existing.merge(toMerge, context)); } - return this; } public boolean hasMultiFields() { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java index 75bc9b9c94ef7..ca15248c037bc 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java @@ -75,7 +75,7 @@ public final String simpleName() { /** * Return the merge of {@code mergeWith} into this. - * Both {@code this} and {@code mergeWith} will be left unmodified. + * Both {@code this} and {@code mergeWith} will be left unmodified. */ public abstract Mapper merge(Mapper mergeWith, MapperMergeContext mapperMergeContext); @@ -135,4 +135,18 @@ public static FieldType freezeAndDeduplicateFieldType(FieldType fieldType) { } return fieldTypeDeduplicator.computeIfAbsent(fieldType, Function.identity()); } + + /** + * Returns the size this mapper counts against the {@linkplain MapperService#INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING field limit}. + *

+ * Needs to be in sync with {@link MappingLookup#getTotalFieldsCount()}. + */ + public int mapperSize() { + int size = 1; + for (Mapper mapper : this) { + size += mapper.mapperSize(); + } + return size; + } + } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java index 699b67858deed..79adaf5966c5b 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java @@ -16,37 +16,106 @@ public final class MapperMergeContext { private final MapperBuilderContext mapperBuilderContext; + private final NewFieldsBudget newFieldsBudget; + + private MapperMergeContext(MapperBuilderContext mapperBuilderContext, NewFieldsBudget newFieldsBudget) { + this.mapperBuilderContext = mapperBuilderContext; + this.newFieldsBudget = newFieldsBudget; + } /** * The root context, to be used when merging a tree of mappers */ - public static MapperMergeContext root(boolean isSourceSynthetic, boolean isDataStream) { - return new MapperMergeContext(MapperBuilderContext.root(isSourceSynthetic, isDataStream)); + public static MapperMergeContext root(boolean isSourceSynthetic, boolean isDataStream, long newFieldsBudget) { + return new MapperMergeContext(MapperBuilderContext.root(isSourceSynthetic, isDataStream), NewFieldsBudget.of(newFieldsBudget)); } /** * Creates a new {@link MapperMergeContext} from a {@link MapperBuilderContext} * @param mapperBuilderContext the {@link MapperBuilderContext} for this {@link MapperMergeContext} + * @param newFieldsBudget limits how many fields can be added during the merge process * @return a new {@link MapperMergeContext}, wrapping the provided {@link MapperBuilderContext} */ - public static MapperMergeContext from(MapperBuilderContext mapperBuilderContext) { - return new MapperMergeContext(mapperBuilderContext); - } - - private MapperMergeContext(MapperBuilderContext mapperBuilderContext) { - this.mapperBuilderContext = mapperBuilderContext; + public static MapperMergeContext from(MapperBuilderContext mapperBuilderContext, long newFieldsBudget) { + return new MapperMergeContext(mapperBuilderContext, NewFieldsBudget.of(newFieldsBudget)); } /** - * Creates a new {@link MapperMergeContext} that is a child of this context + * Creates a new {@link MapperMergeContext} with a child {@link MapperBuilderContext}. + * The child {@link MapperMergeContext} context will share the same field limit. * @param name the name of the child context * @return a new {@link MapperMergeContext} with this context as its parent */ - public MapperMergeContext createChildContext(String name) { - return new MapperMergeContext(mapperBuilderContext.createChildContext(name)); + MapperMergeContext createChildContext(String name) { + return createChildContext(mapperBuilderContext.createChildContext(name)); + } + + /** + * Creates a new {@link MapperMergeContext} with a given child {@link MapperBuilderContext} + * The child {@link MapperMergeContext} context will share the same field limit. + * @param childContext the child {@link MapperBuilderContext} + * @return a new {@link MapperMergeContext}, wrapping the provided {@link MapperBuilderContext} + */ + MapperMergeContext createChildContext(MapperBuilderContext childContext) { + return new MapperMergeContext(childContext, newFieldsBudget); } MapperBuilderContext getMapperBuilderContext() { return mapperBuilderContext; } + + boolean decrementFieldBudgetIfPossible(int fieldSize) { + return newFieldsBudget.decrementIfPossible(fieldSize); + } + + /** + * Keeps track of how many new fields can be added during mapper merge. + * The field budget is shared across instances of {@link MapperMergeContext} that are created via + * {@link MapperMergeContext#createChildContext}. + * This ensures that fields that are consumed by one child object mapper also decrement the budget for another child object. + * Not thread safe.The same instance may not be modified by multiple threads. + */ + private interface NewFieldsBudget { + + static NewFieldsBudget of(long fieldsBudget) { + if (fieldsBudget == Long.MAX_VALUE) { + return Unlimited.INSTANCE; + } + return new Limited(fieldsBudget); + } + + boolean decrementIfPossible(long fieldSize); + + final class Unlimited implements NewFieldsBudget { + + private static final Unlimited INSTANCE = new Unlimited(); + + private Unlimited() {} + + @Override + public boolean decrementIfPossible(long fieldSize) { + return true; + } + + } + + final class Limited implements NewFieldsBudget { + + private long fieldsBudget; + + Limited(long fieldsBudget) { + this.fieldsBudget = fieldsBudget; + } + + @Override + public boolean decrementIfPossible(long fieldSize) { + if (fieldsBudget >= fieldSize) { + fieldsBudget -= fieldSize; + return true; + } + return false; + } + + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index b714eabbd2636..61dd444be34b1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -559,11 +559,15 @@ public Mapping parseMapping(String mappingType, Map mappingSourc } public static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason) { + return mergeMappings(currentMapper, incomingMapping, reason, Long.MAX_VALUE); + } + + static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason, long newFieldsBudget) { Mapping newMapping; if (currentMapper == null) { - newMapping = incomingMapping; + newMapping = incomingMapping.withFieldsBudget(newFieldsBudget); } else { - newMapping = currentMapper.mapping().merge(incomingMapping, reason); + newMapping = currentMapper.mapping().merge(incomingMapping, reason, newFieldsBudget); } return newMapping; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java index 40ecc6af57e8e..903e4e5da5b29 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java @@ -133,10 +133,12 @@ public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { * * @param mergeWith the new mapping to merge into this one. * @param reason the reason this merge was initiated. + * @param newFieldsBudget how many new fields can be added during the merge process * @return the resulting merged mapping. */ - Mapping merge(Mapping mergeWith, MergeReason reason) { - RootObjectMapper mergedRoot = root.merge(mergeWith.root, reason, MapperMergeContext.root(isSourceSynthetic(), false)); + Mapping merge(Mapping mergeWith, MergeReason reason, long newFieldsBudget) { + MapperMergeContext mergeContext = MapperMergeContext.root(isSourceSynthetic(), false, newFieldsBudget); + RootObjectMapper mergedRoot = root.merge(mergeWith.root, reason, mergeContext); // When merging metadata fields as part of applying an index template, new field definitions // completely overwrite existing ones instead of being merged. This behavior matches how we @@ -148,7 +150,7 @@ Mapping merge(Mapping mergeWith, MergeReason reason) { if (mergeInto == null || reason == MergeReason.INDEX_TEMPLATE) { merged = metaMergeWith; } else { - merged = (MetadataFieldMapper) mergeInto.merge(metaMergeWith, MapperMergeContext.root(isSourceSynthetic(), false)); + merged = (MetadataFieldMapper) mergeInto.merge(metaMergeWith, mergeContext); } mergedMetadataMappers.put(merged.getClass(), merged); } @@ -169,6 +171,18 @@ Mapping merge(Mapping mergeWith, MergeReason reason) { return new Mapping(mergedRoot, mergedMetadataMappers.values().toArray(new MetadataFieldMapper[0]), mergedMeta); } + /** + * Returns a copy of this mapper that ensures that the number of fields isn't greater than the provided fields budget. + * @param fieldsBudget the maximum number of fields this mapping may have + */ + public Mapping withFieldsBudget(long fieldsBudget) { + MapperMergeContext mergeContext = MapperMergeContext.root(isSourceSynthetic(), false, fieldsBudget); + // get a copy of the root mapper, without any fields + RootObjectMapper shallowRoot = root.withoutMappers(); + // calling merge on the shallow root to ensure we're only adding as many fields as allowed by the fields budget + return new Mapping(shallowRoot.merge(root, MergeReason.MAPPING_RECOVERY, mergeContext), metadataMappers, meta); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { root.toXContent(builder, params, (b, params1) -> { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java index 4880ce5edc204..0172c22c0b176 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java @@ -271,7 +271,7 @@ private void checkFieldLimit(long limit) { } void checkFieldLimit(long limit, int additionalFieldsToAdd) { - if (getTotalFieldsCount() + additionalFieldsToAdd - mapping.getSortedMetadataMappers().length > limit) { + if (exceedsLimit(limit, additionalFieldsToAdd)) { throw new IllegalArgumentException( "Limit of total fields [" + limit @@ -281,6 +281,10 @@ void checkFieldLimit(long limit, int additionalFieldsToAdd) { } } + boolean exceedsLimit(long limit, int additionalFieldsToAdd) { + return getTotalFieldsCount() + additionalFieldsToAdd - mapping.getSortedMetadataMappers().length > limit; + } + private void checkDimensionFieldLimit(long limit) { long dimensionFieldCount = fieldMappers.values() .stream() diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java index 87a8979934a4e..dd2d4407bed03 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java @@ -186,6 +186,21 @@ public ObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) { return builder; } + @Override + NestedObjectMapper withoutMappers() { + return new NestedObjectMapper( + simpleName(), + fullPath(), + Map.of(), + enabled, + dynamic, + includeInParent, + includeInRoot, + nestedTypePath, + nestedTypeFilter + ); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(simpleName()); @@ -260,7 +275,9 @@ protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeCo if (mapperBuilderContext instanceof NestedMapperBuilderContext == false) { parentIncludedInRoot |= this.includeInParent.value(); } - return MapperMergeContext.from(new NestedMapperBuilderContext(mapperBuilderContext.buildFullName(name), parentIncludedInRoot)); + return mapperMergeContext.createChildContext( + new NestedMapperBuilderContext(mapperBuilderContext.buildFullName(name), parentIncludedInRoot) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 922beb25d8a2f..2c89f60f40975 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -162,7 +162,7 @@ protected final Map buildMappers(MapperBuilderContext mapperBuil // This can also happen due to multiple index templates being merged into a single mappings definition using // XContentHelper#mergeDefaults, again in case some index templates contained mappings for the same field using a // mix of object notation and dot notation. - mapper = existing.merge(mapper, MapperMergeContext.from(mapperBuilderContext)); + mapper = existing.merge(mapper, MapperMergeContext.from(mapperBuilderContext, Long.MAX_VALUE)); } mappers.put(mapper.simpleName(), mapper); } @@ -403,6 +403,14 @@ public ObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) { return builder; } + /** + * Returns a copy of this object mapper that doesn't have any fields and runtime fields. + * This is typically used in the context of a mapper merge when there's not enough budget to add the entire object. + */ + ObjectMapper withoutMappers() { + return new ObjectMapper(simpleName(), fullPath, enabled, subobjects, dynamic, Map.of()); + } + @Override public String name() { return this.fullPath; @@ -542,10 +550,15 @@ private static Map buildMergedMappers( Mapper mergeWithMapper = iterator.next(); Mapper mergeIntoMapper = mergedMappers.get(mergeWithMapper.simpleName()); + Mapper merged = null; if (mergeIntoMapper == null) { - mergedMappers.put(mergeWithMapper.simpleName(), mergeWithMapper); + if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.mapperSize())) { + merged = mergeWithMapper; + } else if (mergeWithMapper instanceof ObjectMapper om) { + merged = truncateObjectMapper(reason, objectMergeContext, om); + } } else if (mergeIntoMapper instanceof ObjectMapper objectMapper) { - mergedMappers.put(objectMapper.simpleName(), objectMapper.merge(mergeWithMapper, reason, objectMergeContext)); + merged = objectMapper.merge(mergeWithMapper, reason, objectMergeContext); } else { assert mergeIntoMapper instanceof FieldMapper || mergeIntoMapper instanceof FieldAliasMapper; if (mergeWithMapper instanceof NestedObjectMapper) { @@ -557,14 +570,28 @@ private static Map buildMergedMappers( // If we're merging template mappings when creating an index, then a field definition always // replaces an existing one. if (reason == MergeReason.INDEX_TEMPLATE) { - mergedMappers.put(mergeWithMapper.simpleName(), mergeWithMapper); + merged = mergeWithMapper; } else { - mergedMappers.put(mergeWithMapper.simpleName(), mergeIntoMapper.merge(mergeWithMapper, objectMergeContext)); + merged = mergeIntoMapper.merge(mergeWithMapper, objectMergeContext); } } + if (merged != null) { + mergedMappers.put(merged.simpleName(), merged); + } } return Map.copyOf(mergedMappers); } + + private static ObjectMapper truncateObjectMapper(MergeReason reason, MapperMergeContext context, ObjectMapper objectMapper) { + // there's not enough capacity for the whole object mapper, + // so we're just trying to add the shallow object, without it's sub-fields + ObjectMapper shallowObjectMapper = objectMapper.withoutMappers(); + if (context.decrementFieldBudgetIfPossible(shallowObjectMapper.mapperSize())) { + // now trying to add the sub-fields one by one via a merge, until we hit the limit + return shallowObjectMapper.merge(objectMapper, reason, context); + } + return null; + } } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java index 05f05dd5be941..014be192a4133 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java @@ -160,7 +160,7 @@ public void addDynamicMappingsUpdate(Mapping update) { if (dynamicMappingsUpdate == null) { dynamicMappingsUpdate = update; } else { - dynamicMappingsUpdate = dynamicMappingsUpdate.merge(update, MergeReason.MAPPING_UPDATE); + dynamicMappingsUpdate = dynamicMappingsUpdate.merge(update, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index 3cc687161b8d6..82cda33aa77b6 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -155,6 +155,22 @@ public RootObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) { return builder; } + @Override + RootObjectMapper withoutMappers() { + return new RootObjectMapper( + simpleName(), + enabled, + subobjects, + dynamic, + Map.of(), + Map.of(), + dynamicDateTimeFormatters, + dynamicTemplates, + dateDetection, + numericDetection + ); + } + /** * Public API */ @@ -242,12 +258,15 @@ public RootObjectMapper merge(Mapper mergeWith, MergeReason reason, MapperMergeC dynamicTemplates = this.dynamicTemplates; } final Map runtimeFields = new HashMap<>(this.runtimeFields); - assert this.runtimeFields != mergeWithObject.runtimeFields; for (Map.Entry runtimeField : mergeWithObject.runtimeFields.entrySet()) { if (runtimeField.getValue() == null) { runtimeFields.remove(runtimeField.getKey()); - } else { + } else if (runtimeFields.containsKey(runtimeField.getKey())) { runtimeFields.put(runtimeField.getKey(), runtimeField.getValue()); + } else { + if (parentMergeContext.decrementFieldBudgetIfPossible(1)) { + runtimeFields.put(runtimeField.getValue().name(), runtimeField.getValue()); + } } } @@ -502,4 +521,13 @@ private static boolean processField( } return false; } + + @Override + public int mapperSize() { + int size = runtimeFields().size(); + for (Mapper mapper : this) { + size += mapper.mapperSize(); + } + return size; + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java index f0458add93c78..e935ae2431131 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java @@ -63,7 +63,7 @@ public void testAddFields() throws Exception { })); MergeReason reason = randomFrom(MergeReason.MAPPING_UPDATE, MergeReason.INDEX_TEMPLATE); - Mapping merged = MapperService.mergeMappings(stage1, stage2.mapping(), reason); + Mapping merged = MapperService.mergeMappings(stage1, stage2.mapping(), reason, Long.MAX_VALUE); // stage1 mapping should not have been modified assertThat(stage1.mappers().getMapper("age"), nullValue()); assertThat(stage1.mappers().getMapper("obj1.prop1"), nullValue()); @@ -81,7 +81,7 @@ public void testMergeObjectDynamic() throws Exception { DocumentMapper withDynamicMapper = createDocumentMapper(topMapping(b -> b.field("dynamic", "false"))); assertThat(withDynamicMapper.mapping().getRoot().dynamic(), equalTo(ObjectMapper.Dynamic.FALSE)); - Mapping merged = MapperService.mergeMappings(mapper, withDynamicMapper.mapping(), MergeReason.MAPPING_UPDATE); + Mapping merged = MapperService.mergeMappings(mapper, withDynamicMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE); assertThat(merged.getRoot().dynamic(), equalTo(ObjectMapper.Dynamic.FALSE)); } @@ -93,14 +93,14 @@ public void testMergeObjectAndNested() throws Exception { { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> MapperService.mergeMappings(objectMapper, nestedMapper.mapping(), reason) + () -> MapperService.mergeMappings(objectMapper, nestedMapper.mapping(), reason, Long.MAX_VALUE) ); assertThat(e.getMessage(), containsString("can't merge a non-nested mapping [obj] with a nested mapping")); } { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> MapperService.mergeMappings(nestedMapper, objectMapper.mapping(), reason) + () -> MapperService.mergeMappings(nestedMapper, objectMapper.mapping(), reason, Long.MAX_VALUE) ); assertThat(e.getMessage(), containsString("can't merge a non-nested mapping [obj] with a nested mapping")); } @@ -235,11 +235,11 @@ public void testMergeMeta() throws IOException { DocumentMapper updatedMapper = createDocumentMapper(fieldMapping(b -> b.field("type", "text"))); - Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE); + Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE); assertThat(merged.getMeta().get("foo"), equalTo("bar")); updatedMapper = createDocumentMapper(topMapping(b -> b.startObject("_meta").field("foo", "new_bar").endObject())); - merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE); + merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.MAPPING_UPDATE, Long.MAX_VALUE); assertThat(merged.getMeta().get("foo"), equalTo("new_bar")); } @@ -262,7 +262,7 @@ public void testMergeMetaForIndexTemplate() throws IOException { assertThat(initMapper.mapping().getMeta(), equalTo(expected)); DocumentMapper updatedMapper = createDocumentMapper(fieldMapping(b -> b.field("type", "text"))); - Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE); + Mapping merged = MapperService.mergeMappings(initMapper, updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE, Long.MAX_VALUE); assertThat(merged.getMeta(), equalTo(expected)); updatedMapper = createDocumentMapper(topMapping(b -> { @@ -278,7 +278,7 @@ public void testMergeMetaForIndexTemplate() throws IOException { } b.endObject(); })); - merged = merged.merge(updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE); + merged = merged.merge(updatedMapper.mapping(), MergeReason.INDEX_TEMPLATE, Long.MAX_VALUE); expected = Map.of("field", "value", "object", Map.of("field1", "value1", "field2", "new_value", "field3", "value3")); assertThat(merged.getMeta(), equalTo(expected)); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java index 07f4c3c1346c4..f816f403be89f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java @@ -34,6 +34,7 @@ public void testParsing() throws IOException { ); DocumentMapper mapper = createDocumentMapper(mapping); assertEquals(mapping, mapper.mappingSource().toString()); + assertEquals(2, mapper.mapping().getRoot().mapperSize()); } public void testParsingWithMissingPath() { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java new file mode 100644 index 0000000000000..9c38487dbdf7b --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/mapper/MapperMergeContextTests.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.index.mapper; + +import org.elasticsearch.test.ESTestCase; + +public class MapperMergeContextTests extends ESTestCase { + + public void testAddFieldIfPossibleUnderLimit() { + MapperMergeContext context = MapperMergeContext.root(false, false, 1); + assertTrue(context.decrementFieldBudgetIfPossible(1)); + assertFalse(context.decrementFieldBudgetIfPossible(1)); + } + + public void testAddFieldIfPossibleAtLimit() { + MapperMergeContext context = MapperMergeContext.root(false, false, 0); + assertFalse(context.decrementFieldBudgetIfPossible(1)); + } + + public void testAddFieldIfPossibleUnlimited() { + MapperMergeContext context = MapperMergeContext.root(false, false, Long.MAX_VALUE); + assertTrue(context.decrementFieldBudgetIfPossible(Integer.MAX_VALUE)); + assertTrue(context.decrementFieldBudgetIfPossible(Integer.MAX_VALUE)); + } + +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java index e39e42bcade53..61d62c1e41969 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.MapperService.MergeReason; @@ -1508,16 +1509,47 @@ public void testMergeNested() { MapperException e = expectThrows( MapperException.class, - () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false)) + () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertThat(e.getMessage(), containsString("[include_in_parent] parameter can't be updated on a nested object mapping")); NestedObjectMapper result = (NestedObjectMapper) firstMapper.merge( secondMapper, MapperService.MergeReason.INDEX_TEMPLATE, - MapperMergeContext.root(false, false) + MapperMergeContext.root(false, false, Long.MAX_VALUE) ); assertFalse(result.isIncludeInParent()); assertTrue(result.isIncludeInRoot()); } + + public void testWithoutMappers() throws IOException { + ObjectMapper shallowObject = createNestedObjectMapperWithAllParametersSet(b -> {}); + ObjectMapper object = createNestedObjectMapperWithAllParametersSet(b -> { + b.startObject("keyword"); + { + b.field("type", "keyword"); + } + b.endObject(); + }); + assertThat(object.withoutMappers().toString(), equalTo(shallowObject.toString())); + } + + private NestedObjectMapper createNestedObjectMapperWithAllParametersSet(CheckedConsumer propertiesBuilder) + throws IOException { + DocumentMapper mapper = createDocumentMapper(mapping(b -> { + b.startObject("nested_object"); + { + b.field("type", "nested"); + b.field("enabled", false); + b.field("dynamic", false); + b.field("include_in_parent", true); + b.field("include_in_root", true); + b.startObject("properties"); + propertiesBuilder.accept(b); + b.endObject(); + } + b.endObject(); + })); + return (NestedObjectMapper) mapper.mapping().getRoot().getMapper("nested_object"); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java index 7cd1577dfabbd..8eb824884a591 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java @@ -41,7 +41,7 @@ public void testMerge() { ObjectMapper mergeWith = createMapping(false, true, true, true); // WHEN merging mappings - final ObjectMapper merged = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false)); + final ObjectMapper merged = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); // THEN "baz" new field is added to merged mapping final ObjectMapper mergedFoo = (ObjectMapper) merged.getMapper("foo"); @@ -63,7 +63,7 @@ public void testMergeWhenDisablingField() { // THEN a MapperException is thrown with an excepted message MapperException e = expectThrows( MapperException.class, - () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false)) + () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertEquals("the [enabled] parameter can't be updated for the object mapping [foo]", e.getMessage()); } @@ -75,7 +75,10 @@ public void testMergeDisabledField() { new ObjectMapper.Builder("disabled", Explicit.IMPLICIT_TRUE) ).build(MapperBuilderContext.root(false, false)); - RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false)); + RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge( + mergeWith, + MapperMergeContext.root(false, false, Long.MAX_VALUE) + ); assertFalse(((ObjectMapper) merged.getMapper("disabled")).isEnabled()); } @@ -84,14 +87,14 @@ public void testMergeEnabled() { MapperException e = expectThrows( MapperException.class, - () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false)) + () -> rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertEquals("the [enabled] parameter can't be updated for the object mapping [disabled]", e.getMessage()); ObjectMapper result = rootObjectMapper.merge( mergeWith, MapperService.MergeReason.INDEX_TEMPLATE, - MapperMergeContext.root(false, false) + MapperMergeContext.root(false, false, Long.MAX_VALUE) ); assertTrue(result.isEnabled()); } @@ -106,14 +109,14 @@ public void testMergeEnabledForRootMapper() { MapperException e = expectThrows( MapperException.class, - () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false)) + () -> firstMapper.merge(secondMapper, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertEquals("the [enabled] parameter can't be updated for the object mapping [" + type + "]", e.getMessage()); ObjectMapper result = firstMapper.merge( secondMapper, MapperService.MergeReason.INDEX_TEMPLATE, - MapperMergeContext.root(false, false) + MapperMergeContext.root(false, false, Long.MAX_VALUE) ); assertFalse(result.isEnabled()); } @@ -128,7 +131,10 @@ public void testMergeDisabledRootMapper() { Collections.singletonMap("test", new TestRuntimeField("test", "long")) ).build(MapperBuilderContext.root(false, false)); - RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false)); + RootObjectMapper merged = (RootObjectMapper) rootObjectMapper.merge( + mergeWith, + MapperMergeContext.root(false, false, Long.MAX_VALUE) + ); assertFalse(merged.isEnabled()); assertEquals(1, merged.runtimeFields().size()); assertEquals("test", merged.runtimeFields().iterator().next().name()); @@ -138,7 +144,7 @@ public void testMergedFieldNamesFieldWithDotsSubobjectsFalseAtRoot() { RootObjectMapper mergeInto = createRootSubobjectFalseLeafWithDots(); RootObjectMapper mergeWith = createRootSubobjectFalseLeafWithDots(); - final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false)); + final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); final KeywordFieldMapper keywordFieldMapper = (KeywordFieldMapper) merged.getMapper("host.name"); assertEquals("host.name", keywordFieldMapper.name()); @@ -153,7 +159,7 @@ public void testMergedFieldNamesFieldWithDotsSubobjectsFalse() { createObjectSubobjectsFalseLeafWithDots() ).build(MapperBuilderContext.root(false, false)); - final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false)); + final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); ObjectMapper foo = (ObjectMapper) merged.getMapper("foo"); ObjectMapper metrics = (ObjectMapper) foo.getMapper("metrics"); @@ -168,7 +174,7 @@ public void testMergedFieldNamesMultiFields() { RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(createTextKeywordMultiField("text")) .build(MapperBuilderContext.root(false, false)); - final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false)); + final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); TextFieldMapper text = (TextFieldMapper) merged.getMapper("text"); assertEquals("text", text.name()); @@ -186,7 +192,7 @@ public void testMergedFieldNamesMultiFieldsWithinSubobjectsFalse() { createObjectSubobjectsFalseLeafWithMultiField() ).build(MapperBuilderContext.root(false, false)); - final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false)); + final ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); ObjectMapper foo = (ObjectMapper) merged.getMapper("foo"); ObjectMapper metrics = (ObjectMapper) foo.getMapper("metrics"); @@ -198,6 +204,113 @@ public void testMergedFieldNamesMultiFieldsWithinSubobjectsFalse() { assertEquals("keyword", fieldMapper.simpleName()); } + public void testMergeWithLimit() { + // GIVEN an enriched mapping with "baz" new field + ObjectMapper mergeWith = createMapping(false, true, true, true); + + // WHEN merging mappings + final ObjectMapper mergedAdd0 = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, 0)); + final ObjectMapper mergedAdd1 = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, 1)); + + // THEN "baz" new field is added to merged mapping + assertEquals(3, rootObjectMapper.mapperSize()); + assertEquals(4, mergeWith.mapperSize()); + assertEquals(3, mergedAdd0.mapperSize()); + assertEquals(4, mergedAdd1.mapperSize()); + } + + public void testMergeWithLimitTruncatedObjectField() { + RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).build(MapperBuilderContext.root(false, false)); + RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("parent", Explicit.IMPLICIT_FALSE).add( + new KeywordFieldMapper.Builder("child1", IndexVersion.current()) + ).add(new KeywordFieldMapper.Builder("child2", IndexVersion.current())) + ).build(MapperBuilderContext.root(false, false)); + + ObjectMapper mergedAdd0 = root.merge(mergeWith, MapperMergeContext.root(false, false, 0)); + ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1)); + ObjectMapper mergedAdd2 = root.merge(mergeWith, MapperMergeContext.root(false, false, 2)); + ObjectMapper mergedAdd3 = root.merge(mergeWith, MapperMergeContext.root(false, false, 3)); + assertEquals(0, root.mapperSize()); + assertEquals(0, mergedAdd0.mapperSize()); + assertEquals(1, mergedAdd1.mapperSize()); + assertEquals(2, mergedAdd2.mapperSize()); + assertEquals(3, mergedAdd3.mapperSize()); + + ObjectMapper parent1 = (ObjectMapper) mergedAdd1.getMapper("parent"); + assertNull(parent1.getMapper("child1")); + assertNull(parent1.getMapper("child2")); + + ObjectMapper parent2 = (ObjectMapper) mergedAdd2.getMapper("parent"); + // the order is not deterministic, but we expect one to be null and the other to be non-null + assertTrue(parent2.getMapper("child1") == null ^ parent2.getMapper("child2") == null); + + ObjectMapper parent3 = (ObjectMapper) mergedAdd3.getMapper("parent"); + assertNotNull(parent3.getMapper("child1")); + assertNotNull(parent3.getMapper("child2")); + } + + public void testMergeSameObjectDifferentFields() { + RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add(new KeywordFieldMapper.Builder("child1", IndexVersion.current())) + ).build(MapperBuilderContext.root(false, false)); + RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add( + new KeywordFieldMapper.Builder("child1", IndexVersion.current()).ignoreAbove(42) + ).add(new KeywordFieldMapper.Builder("child2", IndexVersion.current())) + ).build(MapperBuilderContext.root(false, false)); + + ObjectMapper mergedAdd0 = root.merge(mergeWith, MapperMergeContext.root(false, false, 0)); + ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1)); + assertEquals(2, root.mapperSize()); + assertEquals(2, mergedAdd0.mapperSize()); + assertEquals(3, mergedAdd1.mapperSize()); + + ObjectMapper parent0 = (ObjectMapper) mergedAdd0.getMapper("parent"); + assertNotNull(parent0.getMapper("child1")); + assertEquals(42, ((KeywordFieldMapper) parent0.getMapper("child1")).fieldType().ignoreAbove()); + assertNull(parent0.getMapper("child2")); + + ObjectMapper parent1 = (ObjectMapper) mergedAdd1.getMapper("parent"); + assertNotNull(parent1.getMapper("child1")); + assertEquals(42, ((KeywordFieldMapper) parent1.getMapper("child1")).fieldType().ignoreAbove()); + assertNotNull(parent1.getMapper("child2")); + } + + public void testMergeWithLimitMultiField() { + RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + createTextKeywordMultiField("text", "keyword1") + ).build(MapperBuilderContext.root(false, false)); + RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + createTextKeywordMultiField("text", "keyword2") + ).build(MapperBuilderContext.root(false, false)); + + assertEquals(2, mergeInto.mapperSize()); + assertEquals(2, mergeWith.mapperSize()); + + ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0)); + ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1)); + assertEquals(2, mergedAdd0.mapperSize()); + assertEquals(3, mergedAdd1.mapperSize()); + } + + public void testMergeWithLimitRuntimeField() { + RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).addRuntimeField( + new TestRuntimeField("existing_runtime_field", "keyword") + ).add(createTextKeywordMultiField("text", "keyword1")).build(MapperBuilderContext.root(false, false)); + RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).addRuntimeField( + new TestRuntimeField("existing_runtime_field", "keyword") + ).addRuntimeField(new TestRuntimeField("new_runtime_field", "keyword")).build(MapperBuilderContext.root(false, false)); + + assertEquals(3, mergeInto.mapperSize()); + assertEquals(2, mergeWith.mapperSize()); + + ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0)); + ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1)); + assertEquals(3, mergedAdd0.mapperSize()); + assertEquals(4, mergedAdd1.mapperSize()); + } + private static RootObjectMapper createRootSubobjectFalseLeafWithDots() { FieldMapper.Builder fieldBuilder = new KeywordFieldMapper.Builder("host.name", IndexVersion.current()); FieldMapper fieldMapper = fieldBuilder.build(MapperBuilderContext.root(false, false)); @@ -231,8 +344,12 @@ private ObjectMapper.Builder createObjectSubobjectsFalseLeafWithMultiField() { } private TextFieldMapper.Builder createTextKeywordMultiField(String name) { + return createTextKeywordMultiField(name, "keyword"); + } + + private TextFieldMapper.Builder createTextKeywordMultiField(String name, String multiFieldName) { TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers()); - builder.multiFieldsBuilder.add(new KeywordFieldMapper.Builder("keyword", IndexVersion.current())); + builder.multiFieldsBuilder.add(new KeywordFieldMapper.Builder(multiFieldName, IndexVersion.current())); return builder; } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java index 3c77bf20b37d2..6e958ddbea904 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java @@ -8,14 +8,17 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.common.Explicit; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.index.mapper.ObjectMapper.Dynamic; +import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentType; @@ -23,6 +26,7 @@ import java.util.List; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.nullValue; @@ -123,7 +127,7 @@ public void testMerge() throws IOException { "_doc", new CompressedXContent(BytesReference.bytes(topMapping(b -> b.field("dynamic", "strict")))) ); - Mapping merged = mapper.mapping().merge(mergeWith, reason); + Mapping merged = mapper.mapping().merge(mergeWith, reason, Long.MAX_VALUE); assertEquals(Dynamic.STRICT, merged.getRoot().dynamic()); } @@ -468,7 +472,7 @@ public void testSubobjectsCannotBeUpdated() throws IOException { })))); MapperException exception = expectThrows( MapperException.class, - () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE) + () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE) ); assertEquals("the [subobjects] parameter can't be updated for the object mapping [field]", exception.getMessage()); } @@ -482,7 +486,7 @@ public void testSubobjectsCannotBeUpdatedOnRoot() throws IOException { })))); MapperException exception = expectThrows( MapperException.class, - () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE) + () -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE) ); assertEquals("the [subobjects] parameter can't be updated for the object mapping [_doc]", exception.getMessage()); } @@ -525,4 +529,45 @@ public void testSyntheticSourceDocValuesFieldWithout() throws IOException { assertThat(o.syntheticFieldLoader().docValuesLoader(null, null), nullValue()); assertThat(mapper.mapping().getRoot().syntheticFieldLoader().docValuesLoader(null, null), nullValue()); } + + public void testNestedObjectWithMultiFieldsMapperSize() throws IOException { + ObjectMapper.Builder mapperBuilder = new ObjectMapper.Builder("parent_size_1", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("child_size_2", Explicit.IMPLICIT_TRUE).add( + new TextFieldMapper.Builder("grand_child_size_3", createDefaultIndexAnalyzers()).addMultiField( + new KeywordFieldMapper.Builder("multi_field_size_4", IndexVersion.current()) + ).addMultiField(new KeywordFieldMapper.Builder("multi_field_size_5", IndexVersion.current())) + ) + ); + assertThat(mapperBuilder.build(MapperBuilderContext.root(false, false)).mapperSize(), equalTo(5)); + } + + public void testWithoutMappers() throws IOException { + ObjectMapper shallowObject = createObjectMapperWithAllParametersSet(b -> {}); + ObjectMapper object = createObjectMapperWithAllParametersSet(b -> { + b.startObject("keyword"); + { + b.field("type", "keyword"); + } + b.endObject(); + }); + assertThat(object.withoutMappers().toString(), equalTo(shallowObject.toString())); + } + + private ObjectMapper createObjectMapperWithAllParametersSet(CheckedConsumer propertiesBuilder) + throws IOException { + DocumentMapper mapper = createDocumentMapper(mapping(b -> { + b.startObject("object"); + { + b.field("type", "object"); + b.field("subobjects", false); + b.field("enabled", false); + b.field("dynamic", false); + b.startObject("properties"); + propertiesBuilder.accept(b); + b.endObject(); + } + b.endObject(); + })); + return (ObjectMapper) mapper.mapping().getRoot().getMapper("object"); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java index b2ba6f04849cf..562a30ba4f389 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java @@ -345,7 +345,7 @@ public void testMerging() { {"type":"test_mapper","fixed":true,"fixed2":true,"required":"value"}"""); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> mapper.merge(badMerge, MapperMergeContext.root(false, false)) + () -> mapper.merge(badMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); String expectedError = """ Mapper for [field] conflicts with existing mapper: @@ -358,7 +358,7 @@ public void testMerging() { // TODO: should we have to include 'fixed' here? Or should updates take as 'defaults' the existing values? TestMapper goodMerge = fromMapping(""" {"type":"test_mapper","fixed":false,"variable":"updated","required":"value"}"""); - TestMapper merged = (TestMapper) mapper.merge(goodMerge, MapperMergeContext.root(false, false)); + TestMapper merged = (TestMapper) mapper.merge(goodMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)); assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper)); // original mapping is unaffected assertEquals(""" @@ -376,7 +376,7 @@ public void testMultifields() throws IOException { String addSubField = """ {"type":"test_mapper","variable":"foo","required":"value","fields":{"sub2":{"type":"keyword"}}}"""; TestMapper toMerge = fromMapping(addSubField); - TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false)); + TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)); assertEquals(XContentHelper.stripWhitespace(""" { "field": { @@ -399,7 +399,7 @@ public void testMultifields() throws IOException { TestMapper badToMerge = fromMapping(badSubField); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> merged.merge(badToMerge, MapperMergeContext.root(false, false)) + () -> merged.merge(badToMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertEquals("mapper [field.sub2] cannot be changed from type [keyword] to [binary]", e.getMessage()); } @@ -415,13 +415,13 @@ public void testCopyTo() { TestMapper toMerge = fromMapping(""" {"type":"test_mapper","variable":"updated","required":"value","copy_to":["foo","bar"]}"""); - TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false)); + TestMapper merged = (TestMapper) mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)); assertEquals(""" {"field":{"type":"test_mapper","variable":"updated","required":"value","copy_to":["foo","bar"]}}""", Strings.toString(merged)); TestMapper removeCopyTo = fromMapping(""" {"type":"test_mapper","variable":"updated","required":"value"}"""); - TestMapper noCopyTo = (TestMapper) merged.merge(removeCopyTo, MapperMergeContext.root(false, false)); + TestMapper noCopyTo = (TestMapper) merged.merge(removeCopyTo, MapperMergeContext.root(false, false, Long.MAX_VALUE)); assertEquals(""" {"field":{"type":"test_mapper","variable":"updated","required":"value"}}""", Strings.toString(noCopyTo)); } @@ -487,7 +487,7 @@ public void testCustomSerialization() { TestMapper toMerge = fromMapping(conflict); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> mapper.merge(toMerge, MapperMergeContext.root(false, false)) + () -> mapper.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)) ); assertEquals( "Mapper for [field] conflicts with existing mapper:\n" @@ -576,7 +576,10 @@ public void testAnalyzers() { TestMapper original = mapper; TestMapper toMerge = fromMapping(mapping); - e = expectThrows(IllegalArgumentException.class, () -> original.merge(toMerge, MapperMergeContext.root(false, false))); + e = expectThrows( + IllegalArgumentException.class, + () -> original.merge(toMerge, MapperMergeContext.root(false, false, Long.MAX_VALUE)) + ); assertEquals( "Mapper for [field] conflicts with existing mapper:\n" + "\tCannot update parameter [analyzer] from [default] to [_standard]", e.getMessage() diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index 5bd85a6dcdea7..b2a6651142181 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -19,6 +20,7 @@ import java.util.Collections; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; public class RootObjectMapperTests extends MapperServiceTestCase { @@ -160,6 +162,7 @@ public void testRuntimeSection() throws IOException { })); MapperService mapperService = createMapperService(mapping); assertEquals(mapping, mapperService.documentMapper().mappingSource().toString()); + assertEquals(3, mapperService.documentMapper().mapping().getRoot().mapperSize()); } public void testRuntimeSectionRejectedUpdate() throws IOException { @@ -358,4 +361,51 @@ public void testEmptyType() throws Exception { assertThat(e.getMessage(), containsString("type cannot be an empty string")); } + public void testWithoutMappers() throws IOException { + RootObjectMapper shallowRoot = createRootObjectMapperWithAllParametersSet(b -> {}, b -> {}); + RootObjectMapper root = createRootObjectMapperWithAllParametersSet(b -> { + b.startObject("keyword"); + { + b.field("type", "keyword"); + } + b.endObject(); + }, b -> { + b.startObject("runtime"); + b.startObject("field").field("type", "keyword").endObject(); + b.endObject(); + }); + assertThat(root.withoutMappers().toString(), equalTo(shallowRoot.toString())); + } + + private RootObjectMapper createRootObjectMapperWithAllParametersSet( + CheckedConsumer buildProperties, + CheckedConsumer buildRuntimeFields + ) throws IOException { + DocumentMapper mapper = createDocumentMapper(topMapping(b -> { + b.field("enabled", false); + b.field("subobjects", false); + b.field("dynamic", false); + b.field("date_detection", false); + b.field("numeric_detection", false); + b.field("dynamic_date_formats", Collections.singletonList("yyyy-MM-dd")); + b.startArray("dynamic_templates"); + { + b.startObject(); + { + b.startObject("my_template"); + { + b.startObject("mapping").field("type", "keyword").endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endArray(); + b.startObject("properties"); + buildProperties.accept(b); + b.endObject(); + })); + return mapper.mapping().getRoot(); + } + }