diff --git a/.gitignore b/.gitignore index cac5a799012e1..f72dde1cac3d1 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,12 @@ server/src/main/resources/transport/defined/manifest.txt # JEnv .java-version + +# Claude Flow and Swarm directories +.claude-flow/ +.swarm/ + +# Temporary test files +test-*.java +bug-*.json +*-bug-analysis.md 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 a43575b8f990c..73d5ae6debc68 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -332,6 +332,10 @@ public final void validate(MappingLookup mappers) { throw new IllegalArgumentException("[copy_to] may not be used to copy from a multi-field: [" + this.fullPath() + "]"); } + // Check if dynamic mappings are disabled + ObjectMapper.Dynamic rootDynamic = ObjectMapper.Dynamic.getRootDynamic(mappers); + boolean isDynamicDisabled = rootDynamic == ObjectMapper.Dynamic.FALSE; + final String sourceScope = mappers.nestedLookup().getNestedParent(this.fullPath()); for (String copyTo : this.copyTo().copyToFields()) { if (mappers.isMultiField(copyTo)) { @@ -341,6 +345,13 @@ public final void validate(MappingLookup mappers) { throw new IllegalArgumentException("Cannot copy to field [" + copyTo + "] since it is mapped as an object"); } + // When dynamic is false, check if the target field exists + if (isDynamicDisabled && mappers.getMapper(copyTo) == null) { + throw new IllegalArgumentException( + "Cannot copy to field [" + copyTo + "] because it does not exist and dynamic mappings are disabled" + ); + } + final String targetScope = mappers.nestedLookup().getNestedParent(copyTo); checkNestedScopeCompatibility(sourceScope, targetScope); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperTests.java index aa184ddf465d5..e4f6f07a5d00c 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperTests.java @@ -32,6 +32,62 @@ public class CopyToMapperTests extends MapperServiceTestCase { + public void testCopyToNonExistentFieldWithDynamicFalse() { + Exception e = expectThrows(IllegalArgumentException.class, () -> createDocumentMapper(topMapping(b -> { + b.field("dynamic", false); + b.startObject("properties"); + { + b.startObject("test_field"); + { + b.field("type", "text"); + b.field("copy_to", "missing_field"); + } + b.endObject(); + } + b.endObject(); + }))); + assertThat( + e.getMessage(), + equalTo("Cannot copy to field [missing_field] because it does not exist and dynamic mappings are disabled") + ); + } + + public void testCopyToExistingFieldWithDynamicFalse() throws Exception { + // This should succeed as the target field exists + DocumentMapper mapper = createDocumentMapper(topMapping(b -> { + b.field("dynamic", false); + b.startObject("properties"); + { + b.startObject("test_field"); + { + b.field("type", "text"); + b.field("copy_to", "target_field"); + } + b.endObject(); + b.startObject("target_field"); + { + b.field("type", "text"); + } + b.endObject(); + } + b.endObject(); + })); + assertNotNull(mapper); + } + + public void testCopyToNonExistentFieldWithDynamicTrue() throws Exception { + // This should succeed as dynamic is true (default) + MapperService mapperService = createMapperService(mapping(b -> { + b.startObject("test_field"); + { + b.field("type", "text"); + b.field("copy_to", "missing_field"); + } + b.endObject(); + })); + assertNotNull(mapperService.documentMapper()); + } + @SuppressWarnings("unchecked") public void testCopyToFieldsParsing() throws Exception {