Browse Source

Support camel case dates on 7.x indices (#88914)

This adds back compatibility support for camel case dates
for 7.x indices used in 8.x.
Nikola Grcevski 3 years ago
parent
commit
5af8ec52fe

+ 6 - 0
docs/changelog/88914.yaml

@@ -0,0 +1,6 @@
+pr: 88914
+summary: Support camel case dates on 7.x indices
+area: Infra/Core
+type: bug
+issues:
+ - 84199

+ 19 - 0
qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/20_camel_case_on_format.yml

@@ -0,0 +1,19 @@
+---
+"Verify that we can still use index with camel case date field":
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "camel_case_on_format"}}'
+          - '{"date_field": "2019-02-01T00:00+01:00"}'
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: camel_case_on_format
+        body:
+          query:
+            range:
+              date_field:
+                gte: "2019-01-01T00:00+01:00"
+                lte: "2019-03-01T00:00+01:00"

+ 79 - 0
qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_camel_case_on_format.yml

@@ -0,0 +1,79 @@
+---
+"Create index with camel case on format (allowed with warning in 7.x)":
+  - skip:
+      version: "8.0.0 - "
+      reason:  "at version 8.0.0, camel case is not allowed"
+      features: "warnings"
+  - do:
+      warnings:
+        - "Camel case format name strictDateOptionalTime is deprecated and will be removed in a future version. Use snake case name strict_date_optional_time instead."
+      indices.create:
+        index: camel_case_on_format
+        body:
+          settings:
+            index:
+              number_of_replicas: 2
+          mappings:
+            "properties":
+              "date_field":
+                "type": "date"
+                "format": "strictDateOptionalTime"
+
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "camel_case_on_format"}}'
+          - '{"date_field": "2019-02-01T00:00+01:00"}'
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: camel_case_on_format
+        body:
+          query:
+            range:
+              date_field:
+                gte: "2019-01-01T00:00+01:00"
+                lte: "2019-03-01T00:00+01:00"
+  - match: { hits.total: 1 }
+
+---
+"Create index with camel case on format (when bwc version is > 8.0.0)":
+  - skip:
+      version: " - 7.99.99"
+      reason:  "at version 8.0.0, camel case is not allowed"
+      features: "warnings"
+  - do:
+      indices.create:
+        index: camel_case_on_format
+        body:
+          settings:
+            index:
+              number_of_replicas: 2
+          mappings:
+            "properties":
+              "date_field":
+                "type": "date"
+                "format": "strict_date_optional_time"
+
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "camel_case_on_format"}}'
+          - '{"date_field": "2019-02-01T00:00+01:00"}'
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: camel_case_on_format
+        body:
+          query:
+            range:
+              date_field:
+                gte: "2019-01-01T00:00+01:00"
+                lte: "2019-03-01T00:00+01:00"
+  - match: { hits.total: 1 }

+ 20 - 0
qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/20_camel_case_on_format.yml

@@ -0,0 +1,20 @@
+---
+"Verify that we can use index with camel case date field in upgraded cluster":
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "camel_case_on_format"}}'
+          - '{"date_field": "2019-02-01T00:00+01:00"}'
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: camel_case_on_format
+        body:
+          query:
+            range:
+              date_field:
+                gte: "2019-01-01T00:00+01:00"
+                lte: "2019-03-01T00:00+01:00"
+  - match: { hits.total: 4 }

+ 24 - 6
server/src/main/java/org/elasticsearch/common/time/DateFormatter.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.common.time;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.common.Strings;
 
 import java.time.Instant;
@@ -18,6 +19,7 @@ import java.time.temporal.TemporalAccessor;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Collectors;
 
 public interface DateFormatter {
 
@@ -99,6 +101,10 @@ public interface DateFormatter {
     DateMathParser toDateMathParser();
 
     static DateFormatter forPattern(String input) {
+        return forPattern(input, Version.CURRENT);
+    }
+
+    static DateFormatter forPattern(String input, Version supportedVersion) {
         if (Strings.hasLength(input) == false) {
             throw new IllegalArgumentException("No date pattern provided");
         }
@@ -108,13 +114,14 @@ public interface DateFormatter {
             input = input.substring(1);
         }
 
-        List<DateFormatter> formatters = new ArrayList<>();
-        for (String pattern : Strings.delimitedListToStringArray(input, "||")) {
-            if (Strings.hasLength(pattern) == false) {
-                throw new IllegalArgumentException("Cannot have empty element in multi date format pattern: " + input);
+        List<String> patterns = splitCombinedPatterns(input);
+        List<DateFormatter> formatters = patterns.stream().map(p -> {
+            // make sure we still support camel case for indices created before 8.0
+            if (supportedVersion.before(Version.V_8_0_0)) {
+                return LegacyFormatNames.camelCaseToSnakeCase(p);
             }
-            formatters.add(DateFormatters.forPattern(pattern));
-        }
+            return p;
+        }).map(DateFormatters::forPattern).collect(Collectors.toList());
 
         if (formatters.size() == 1) {
             return formatters.get(0);
@@ -122,4 +129,15 @@ public interface DateFormatter {
 
         return JavaDateFormatter.combined(input, formatters);
     }
+
+    static List<String> splitCombinedPatterns(String input) {
+        List<String> patterns = new ArrayList<>();
+        for (String pattern : Strings.delimitedListToStringArray(input, "||")) {
+            if (Strings.hasLength(pattern) == false) {
+                throw new IllegalArgumentException("Cannot have empty element in multi date format pattern: " + input);
+            }
+            patterns.add(pattern);
+        }
+        return patterns;
+    }
 }

+ 135 - 0
server/src/main/java/org/elasticsearch/common/time/LegacyFormatNames.java

@@ -0,0 +1,135 @@
+/*
+ * 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.common.time;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public enum LegacyFormatNames {
+    ISO8601(null, "iso8601"),
+    BASIC_DATE("basicDate", "basic_date"),
+    BASIC_DATE_TIME("basicDateTime", "basic_date_time"),
+    BASIC_DATE_TIME_NO_MILLIS("basicDateTimeNoMillis", "basic_date_time_no_millis"),
+    BASIC_ORDINAL_DATE("basicOrdinalDate", "basic_ordinal_date"),
+    BASIC_ORDINAL_DATE_TIME("basicOrdinalDateTime", "basic_ordinal_date_time"),
+    BASIC_ORDINAL_DATE_TIME_NO_MILLIS("basicOrdinalDateTimeNoMillis", "basic_ordinal_date_time_no_millis"),
+    BASIC_TIME("basicTime", "basic_time"),
+    BASIC_TIME_NO_MILLIS("basicTimeNoMillis", "basic_time_no_millis"),
+    BASIC_T_TIME("basicTTime", "basic_t_time"),
+    BASIC_T_TIME_NO_MILLIS("basicTTimeNoMillis", "basic_t_time_no_millis"),
+    BASIC_WEEK_DATE("basicWeekDate", "basic_week_date"),
+    BASIC_WEEK_DATE_TIME("basicWeekDateTime", "basic_week_date_time"),
+    BASIC_WEEK_DATE_TIME_NO_MILLIS("basicWeekDateTimeNoMillis", "basic_week_date_time_no_millis"),
+    DATE(null, "date"),
+    DATE_HOUR("dateHour", "date_hour"),
+    DATE_HOUR_MINUTE("dateHourMinute", "date_hour_minute"),
+    DATE_HOUR_MINUTE_SECOND("dateHourMinuteSecond", "date_hour_minute_second"),
+    DATE_HOUR_MINUTE_SECOND_FRACTION("dateHourMinuteSecondFraction", "date_hour_minute_second_fraction"),
+    DATE_HOUR_MINUTE_SECOND_MILLIS("dateHourMinuteSecondMillis", "date_hour_minute_second_millis"),
+    DATE_OPTIONAL_TIME("dateOptionalTime", "date_optional_time"),
+    DATE_TIME("dateTime", "date_time"),
+    DATE_TIME_NO_MILLIS("dateTimeNoMillis", "date_time_no_millis"),
+    HOUR(null, "hour"),
+    HOUR_MINUTE("hourMinute", "hour_minute"),
+    HOUR_MINUTE_SECOND("hourMinuteSecond", "hour_minute_second"),
+    HOUR_MINUTE_SECOND_FRACTION("hourMinuteSecondFraction", "hour_minute_second_fraction"),
+    HOUR_MINUTE_SECOND_MILLIS("hourMinuteSecondMillis", "hour_minute_second_millis"),
+    ORDINAL_DATE("ordinalDate", "ordinal_date"),
+    ORDINAL_DATE_TIME("ordinalDateTime", "ordinal_date_time"),
+    ORDINAL_DATE_TIME_NO_MILLIS("ordinalDateTimeNoMillis", "ordinal_date_time_no_millis"),
+    TIME(null, "time"),
+    TIME_NO_MILLIS("timeNoMillis", "time_no_millis"),
+    T_TIME("tTime", "t_time"),
+    T_TIME_NO_MILLIS("tTimeNoMillis", "t_time_no_millis"),
+    WEEK_DATE("weekDate", "week_date"),
+    WEEK_DATE_TIME("weekDateTime", "week_date_time"),
+    WEEK_DATE_TIME_NO_MILLIS("weekDateTimeNoMillis", "week_date_time_no_millis"),
+    WEEK_YEAR(null, "week_year"),
+    WEEKYEAR(null, "weekyear"),
+    WEEK_YEAR_WEEK("weekyearWeek", "weekyear_week"),
+    WEEKYEAR_WEEK_DAY("weekyearWeekDay", "weekyear_week_day"),
+    YEAR(null, "year"),
+    YEAR_MONTH("yearMonth", "year_month"),
+    YEAR_MONTH_DAY("yearMonthDay", "year_month_day"),
+    EPOCH_SECOND(null, "epoch_second"),
+    EPOCH_MILLIS(null, "epoch_millis"),
+    // strict date formats here, must be at least 4 digits for year and two for months and two for day
+    STRICT_BASIC_WEEK_DATE("strictBasicWeekDate", "strict_basic_week_date"),
+    STRICT_BASIC_WEEK_DATE_TIME("strictBasicWeekDateTime", "strict_basic_week_date_time"),
+    STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS("strictBasicWeekDateTimeNoMillis", "strict_basic_week_date_time_no_millis"),
+    STRICT_DATE("strictDate", "strict_date"),
+    STRICT_DATE_HOUR("strictDateHour", "strict_date_hour"),
+    STRICT_DATE_HOUR_MINUTE("strictDateHourMinute", "strict_date_hour_minute"),
+    STRICT_DATE_HOUR_MINUTE_SECOND("strictDateHourMinuteSecond", "strict_date_hour_minute_second"),
+    STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION("strictDateHourMinuteSecondFraction", "strict_date_hour_minute_second_fraction"),
+    STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS("strictDateHourMinuteSecondMillis", "strict_date_hour_minute_second_millis"),
+    STRICT_DATE_OPTIONAL_TIME("strictDateOptionalTime", "strict_date_optional_time"),
+    STRICT_DATE_OPTIONAL_TIME_NANOS("strictDateOptionalTimeNanos", "strict_date_optional_time_nanos"),
+    STRICT_DATE_TIME("strictDateTime", "strict_date_time"),
+    STRICT_DATE_TIME_NO_MILLIS("strictDateTimeNoMillis", "strict_date_time_no_millis"),
+    STRICT_HOUR("strictHour", "strict_hour"),
+    STRICT_HOUR_MINUTE("strictHourMinute", "strict_hour_minute"),
+    STRICT_HOUR_MINUTE_SECOND("strictHourMinuteSecond", "strict_hour_minute_second"),
+    STRICT_HOUR_MINUTE_SECOND_FRACTION("strictHourMinuteSecondFraction", "strict_hour_minute_second_fraction"),
+    STRICT_HOUR_MINUTE_SECOND_MILLIS("strictHourMinuteSecondMillis", "strict_hour_minute_second_millis"),
+    STRICT_ORDINAL_DATE("strictOrdinalDate", "strict_ordinal_date"),
+    STRICT_ORDINAL_DATE_TIME("strictOrdinalDateTime", "strict_ordinal_date_time"),
+    STRICT_ORDINAL_DATE_TIME_NO_MILLIS("strictOrdinalDateTimeNoMillis", "strict_ordinal_date_time_no_millis"),
+    STRICT_TIME("strictTime", "strict_time"),
+    STRICT_TIME_NO_MILLIS("strictTimeNoMillis", "strict_time_no_millis"),
+    STRICT_T_TIME("strictTTime", "strict_t_time"),
+    STRICT_T_TIME_NO_MILLIS("strictTTimeNoMillis", "strict_t_time_no_millis"),
+    STRICT_WEEK_DATE("strictWeekDate", "strict_week_date"),
+    STRICT_WEEK_DATE_TIME("strictWeekDateTime", "strict_week_date_time"),
+    STRICT_WEEK_DATE_TIME_NO_MILLIS("strictWeekDateTimeNoMillis", "strict_week_date_time_no_millis"),
+    STRICT_WEEKYEAR("strictWeekyear", "strict_weekyear"),
+    STRICT_WEEKYEAR_WEEK("strictWeekyearWeek", "strict_weekyear_week"),
+    STRICT_WEEKYEAR_WEEK_DAY("strictWeekyearWeekDay", "strict_weekyear_week_day"),
+    STRICT_YEAR("strictYear", "strict_year"),
+    STRICT_YEAR_MONTH("strictYearMonth", "strict_year_month"),
+    STRICT_YEAR_MONTH_DAY("strictYearMonthDay", "strict_year_month_day");
+
+    private static final Map<String, String> ALL_NAMES = Arrays.stream(values())
+        .filter(n -> n.camelCaseName != null)
+        .collect(Collectors.toMap(n -> n.camelCaseName, n -> n.snakeCaseName));
+
+    private final String camelCaseName;
+    private final String snakeCaseName;
+
+    LegacyFormatNames(String camelCaseName, String snakeCaseName) {
+        this.camelCaseName = camelCaseName;
+        this.snakeCaseName = snakeCaseName;
+    }
+
+    public static LegacyFormatNames forName(String format) {
+        for (var name : values()) {
+            if (name.matches(format)) {
+                return name;
+            }
+        }
+        return null;
+    }
+
+    public boolean isCamelCase(String format) {
+        return format.equals(camelCaseName);
+    }
+
+    public String getSnakeCaseName() {
+        return snakeCaseName;
+    }
+
+    public boolean matches(String format) {
+        return format.equals(camelCaseName) || format.equals(snakeCaseName);
+    }
+
+    public static String camelCaseToSnakeCase(String format) {
+        return ALL_NAMES.getOrDefault(format, format);
+    }
+}

+ 18 - 3
server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java

@@ -14,6 +14,7 @@ import org.apache.lucene.queries.spans.SpanQuery;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.Fuzziness;
@@ -236,10 +237,10 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
         @Override
         protected final RuntimeField createRuntimeField(MappingParserContext parserContext) {
             if (script.get() == null) {
-                return createRuntimeField(getParseFromSourceFactory());
+                return createRuntimeField(getParseFromSourceFactory(), parserContext.indexVersionCreated());
             }
             Factory factory = parserContext.scriptCompiler().compile(script.getValue(), scriptContext);
-            return createRuntimeField(factory);
+            return createRuntimeField(factory, parserContext.indexVersionCreated());
         }
 
         @Override
@@ -262,12 +263,26 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
         }
 
         final RuntimeField createRuntimeField(Factory scriptFactory) {
-            AbstractScriptFieldType<?> fieldType = createFieldType(name, scriptFactory, getScript(), meta());
+            return createRuntimeField(scriptFactory, Version.CURRENT);
+        }
+
+        final RuntimeField createRuntimeField(Factory scriptFactory, Version indexVersion) {
+            var fieldType = createFieldType(name, scriptFactory, getScript(), meta(), indexVersion);
             return new LeafRuntimeField(name, fieldType, getParameters());
         }
 
         abstract AbstractScriptFieldType<?> createFieldType(String name, Factory factory, Script script, Map<String, String> meta);
 
+        AbstractScriptFieldType<?> createFieldType(
+            String name,
+            Factory factory,
+            Script script,
+            Map<String, String> meta,
+            Version supportedVersion
+        ) {
+            return createFieldType(name, factory, script, meta);
+        }
+
         @Override
         protected List<FieldMapper.Parameter<?>> getParameters() {
             List<FieldMapper.Parameter<?>> parameters = new ArrayList<>(super.getParameters());

+ 2 - 2
server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

@@ -281,9 +281,9 @@ public final class DateFieldMapper extends FieldMapper {
             }
         }
 
-        private DateFormatter buildFormatter() {
+        DateFormatter buildFormatter() {
             try {
-                return DateFormatter.forPattern(format.getValue()).withLocale(locale.getValue());
+                return DateFormatter.forPattern(format.getValue(), indexCreatedVersion).withLocale(locale.getValue());
             } catch (IllegalArgumentException e) {
                 if (indexCreatedVersion.isLegacyIndexVersion()) {
                     logger.warn(() -> "Error parsing format [" + format.getValue() + "] of legacy index, falling back to default", e);

+ 16 - 4
server/src/main/java/org/elasticsearch/index/mapper/DateScriptFieldType.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.search.Query;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.common.time.DateFormatter;
 import org.elasticsearch.common.time.DateMathParser;
@@ -87,13 +88,24 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
         }
 
         @Override
-        AbstractScriptFieldType<?> createFieldType(String name, DateFieldScript.Factory factory, Script script, Map<String, String> meta) {
+        AbstractScriptFieldType<?> createFieldType(
+            String name,
+            DateFieldScript.Factory factory,
+            Script script,
+            Map<String, String> meta,
+            Version supportedVersion
+        ) {
             String pattern = format.getValue() == null ? DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern() : format.getValue();
             Locale locale = this.locale.getValue() == null ? Locale.ROOT : this.locale.getValue();
-            DateFormatter dateTimeFormatter = DateFormatter.forPattern(pattern).withLocale(locale);
+            DateFormatter dateTimeFormatter = DateFormatter.forPattern(pattern, supportedVersion).withLocale(locale);
             return new DateScriptFieldType(name, factory, dateTimeFormatter, script, meta);
         }
 
+        @Override
+        AbstractScriptFieldType<?> createFieldType(String name, DateFieldScript.Factory factory, Script script, Map<String, String> meta) {
+            return createFieldType(name, factory, script, meta, Version.CURRENT);
+        }
+
         @Override
         DateFieldScript.Factory getParseFromSourceFactory() {
             return DateFieldScript.PARSE_FROM_SOURCE;
@@ -105,10 +117,10 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
         }
     }
 
-    public static RuntimeField sourceOnly(String name, DateFormatter dateTimeFormatter) {
+    public static RuntimeField sourceOnly(String name, DateFormatter dateTimeFormatter, Version supportedVersion) {
         Builder builder = new Builder(name);
         builder.format.setValue(dateTimeFormatter.pattern());
-        return builder.createRuntimeField(DateFieldScript.PARSE_FROM_SOURCE);
+        return builder.createRuntimeField(DateFieldScript.PARSE_FROM_SOURCE, supportedVersion);
     }
 
     private final DateFormatter dateTimeFormatter;

+ 3 - 1
server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java

@@ -418,7 +418,9 @@ final class DynamicFieldsBuilder {
         @Override
         public void newDynamicDateField(DocumentParserContext context, String name, DateFormatter dateFormatter) {
             String fullName = context.path().pathAsText(name);
-            createDynamicField(DateScriptFieldType.sourceOnly(fullName, dateFormatter), context);
+            MappingParserContext parserContext = context.dynamicTemplateParserContext(dateFormatter);
+
+            createDynamicField(DateScriptFieldType.sourceOnly(fullName, dateFormatter, parserContext.indexVersionCreated()), context);
         }
     }
 }

+ 47 - 0
server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java

@@ -18,7 +18,9 @@ import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType;
 import org.elasticsearch.index.termvectors.TermVectorsService;
 import org.elasticsearch.script.DateFieldScript;
+import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.search.DocValueFormat;
+import org.elasticsearch.test.VersionUtils;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
@@ -41,6 +43,7 @@ import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.Mockito.mock;
 
 public class DateFieldMapperTests extends MapperTestCase {
 
@@ -724,4 +727,48 @@ public class DateFieldMapperTests extends MapperTestCase {
         assertThat(service.fieldType("mydate"), instanceOf(DateFieldType.class));
         assertNotEquals(DEFAULT_DATE_TIME_FORMATTER, ((DateFieldType) service.fieldType("mydate")).dateTimeFormatter);
     }
+
+    public void testLegacyDateFormatName() {
+        DateFieldMapper.Builder builder = new DateFieldMapper.Builder(
+            "format",
+            DateFieldMapper.Resolution.MILLISECONDS,
+            null,
+            mock(ScriptService.class),
+            true,
+            VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, VersionUtils.getPreviousVersion(Version.V_8_0_0)) // BWC compatible
+                                                                                                                           // index, e.g 7.x
+        );
+
+        // Check that we allow the use of camel case date formats on 7.x indices
+        @SuppressWarnings("unchecked")
+        FieldMapper.Parameter<String> formatParam = (FieldMapper.Parameter<String>) builder.getParameters()[3];
+        formatParam.parse("date_time_format", mock(MappingParserContext.class), "strictDateOptionalTime");
+        builder.buildFormatter(); // shouldn't throw exception
+
+        formatParam.parse("date_time_format", mock(MappingParserContext.class), "strictDateOptionalTime||strictDateOptionalTimeNanos");
+        builder.buildFormatter(); // shouldn't throw exception
+
+        DateFieldMapper.Builder newFieldBuilder = new DateFieldMapper.Builder(
+            "format",
+            DateFieldMapper.Resolution.MILLISECONDS,
+            null,
+            mock(ScriptService.class),
+            true,
+            Version.CURRENT
+        );
+
+        @SuppressWarnings("unchecked")
+        final FieldMapper.Parameter<String> newFormatParam = (FieldMapper.Parameter<String>) newFieldBuilder.getParameters()[3];
+
+        // Check that we don't allow the use of camel case date formats on 8.x indices
+        assertEquals(
+            "Error parsing [format] on field [format]: Invalid format: [strictDateOptionalTime]: Unknown pattern letter: t",
+            expectThrows(IllegalArgumentException.class, () -> {
+                newFormatParam.parse("date_time_format", mock(MappingParserContext.class), "strictDateOptionalTime");
+                assertEquals("strictDateOptionalTime", newFormatParam.getValue());
+                newFieldBuilder.buildFormatter();
+            }).getMessage()
+        );
+
+    }
 }

+ 15 - 0
server/src/test/java/org/elasticsearch/index/mapper/DateScriptFieldTypeTests.java

@@ -431,6 +431,21 @@ public class DateScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTest
         }
     }
 
+    public void testLegacyDateFormatName() throws IOException {
+        CheckedSupplier<XContentBuilder, IOException> mapping = () -> runtimeFieldMapping(b -> {
+            minimalMapping(b);
+            b.field("format", "strictDateOptionalTime");
+        });
+        // Check that we can correctly use the camel case date format for 7.x indices
+        createMapperService(Version.fromId(7_99_99_99), mapping.get()); // no exception thrown
+
+        // Check that we don't allow the use of camel case date formats on 8.x indices
+        assertEquals(
+            "Failed to parse mapping: Invalid format: [strictDateOptionalTime]: Unknown pattern letter: t",
+            expectThrows(MapperParsingException.class, () -> { createMapperService(mapping.get()); }).getMessage()
+        );
+    }
+
     @Override
     protected Query randomTermsQuery(MappedFieldType ft, SearchExecutionContext ctx) {
         return ft.termsQuery(randomList(1, 100, DateScriptFieldTypeTests::randomDate), ctx);

+ 2 - 1
x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java

@@ -99,7 +99,8 @@ public class DeprecationChecks {
         IndexDeprecationChecks::translogRetentionSettingCheck,
         IndexDeprecationChecks::checkIndexDataPath,
         IndexDeprecationChecks::storeTypeSettingCheck,
-        IndexDeprecationChecks::frozenIndexSettingCheck
+        IndexDeprecationChecks::frozenIndexSettingCheck,
+        IndexDeprecationChecks::deprecatedCamelCasePattern
     );
 
     /**

+ 137 - 0
x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java

@@ -8,6 +8,9 @@ package org.elasticsearch.xpack.deprecation;
 
 import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.MappingMetadata;
+import org.elasticsearch.common.time.DateFormatter;
+import org.elasticsearch.common.time.LegacyFormatNames;
 import org.elasticsearch.index.IndexModule;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.engine.frozen.FrozenEngine;
@@ -17,6 +20,10 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * Index-specific deprecation checks
@@ -115,4 +122,134 @@ public class IndexDeprecationChecks {
         }
         return null;
     }
+
+    private static void fieldLevelMappingIssue(IndexMetadata indexMetadata, BiConsumer<MappingMetadata, Map<String, Object>> checker) {
+        if (indexMetadata.mapping() != null) {
+            Map<String, Object> sourceAsMap = indexMetadata.mapping().sourceAsMap();
+            checker.accept(indexMetadata.mapping(), sourceAsMap);
+        }
+    }
+
+    /**
+     * iterates through the "properties" field of mappings and returns any predicates that match in the
+     * form of issue-strings.
+     *
+     * @param type the document type
+     * @param parentMap the mapping to read properties from
+     * @param predicate the predicate to check against for issues, issue is returned if predicate evaluates to true
+     * @param fieldFormatter a function that takes a type and mapping field entry and returns a formatted field representation
+     * @return a list of issues found in fields
+     */
+    @SuppressWarnings("unchecked")
+    static List<String> findInPropertiesRecursively(
+        String type,
+        Map<String, Object> parentMap,
+        Function<Map<?, ?>, Boolean> predicate,
+        BiFunction<String, Map.Entry<?, ?>, String> fieldFormatter,
+        String fieldBeginMarker,
+        String fieldEndMarker
+    ) {
+        List<String> issues = new ArrayList<>();
+        Map<?, ?> properties = (Map<?, ?>) parentMap.get("properties");
+        if (properties == null) {
+            return issues;
+        }
+        for (Map.Entry<?, ?> entry : properties.entrySet()) {
+            Map<String, Object> valueMap = (Map<String, Object>) entry.getValue();
+            if (predicate.apply(valueMap)) {
+                issues.add(fieldBeginMarker + fieldFormatter.apply(type, entry) + fieldEndMarker);
+            }
+
+            Map<?, ?> values = (Map<?, ?>) valueMap.get("fields");
+            if (values != null) {
+                for (Map.Entry<?, ?> multifieldEntry : values.entrySet()) {
+                    Map<String, Object> multifieldValueMap = (Map<String, Object>) multifieldEntry.getValue();
+                    if (predicate.apply(multifieldValueMap)) {
+                        issues.add(
+                            fieldBeginMarker
+                                + fieldFormatter.apply(type, entry)
+                                + ", multifield: "
+                                + multifieldEntry.getKey()
+                                + fieldEndMarker
+                        );
+                    }
+                    if (multifieldValueMap.containsKey("properties")) {
+                        issues.addAll(
+                            findInPropertiesRecursively(
+                                type,
+                                multifieldValueMap,
+                                predicate,
+                                fieldFormatter,
+                                fieldBeginMarker,
+                                fieldEndMarker
+                            )
+                        );
+                    }
+                }
+            }
+            if (valueMap.containsKey("properties")) {
+                issues.addAll(findInPropertiesRecursively(type, valueMap, predicate, fieldFormatter, fieldBeginMarker, fieldEndMarker));
+            }
+        }
+
+        return issues;
+    }
+
+    static DeprecationIssue deprecatedCamelCasePattern(IndexMetadata indexMetadata) {
+        List<String> fields = new ArrayList<>();
+        fieldLevelMappingIssue(
+            indexMetadata,
+            ((mappingMetadata, sourceAsMap) -> fields.addAll(
+                findInPropertiesRecursively(
+                    mappingMetadata.type(),
+                    sourceAsMap,
+                    IndexDeprecationChecks::isDateFieldWithCamelCasePattern,
+                    IndexDeprecationChecks::changeFormatToSnakeCase,
+                    "",
+                    ""
+                )
+            ))
+        );
+
+        if (fields.size() > 0) {
+            String detailsMessageBeginning = fields.stream().collect(Collectors.joining(" "));
+            return new DeprecationIssue(
+                DeprecationIssue.Level.CRITICAL,
+                "Date fields use deprecated camel case formats",
+                "https://ela.st/es-deprecation-7-camel-case-format",
+                detailsMessageBeginning,
+                false,
+                null
+            );
+        }
+        return null;
+    }
+
+    private static boolean isDateFieldWithCamelCasePattern(Map<?, ?> property) {
+        if ("date".equals(property.get("type")) && property.containsKey("format")) {
+            List<String> patterns = DateFormatter.splitCombinedPatterns((String) property.get("format"));
+            for (String pattern : patterns) {
+                LegacyFormatNames format = LegacyFormatNames.forName(pattern);
+                return format != null && format.isCamelCase(pattern);
+            }
+        }
+        return false;
+    }
+
+    private static String changeFormatToSnakeCase(String type, Map.Entry<?, ?> entry) {
+        Map<?, ?> value = (Map<?, ?>) entry.getValue();
+        final String formatFieldValue = (String) value.get("format");
+        List<String> patterns = DateFormatter.splitCombinedPatterns(formatFieldValue);
+        StringBuilder sb = new StringBuilder(
+            "Convert [" + entry.getKey() + "] format [" + formatFieldValue + "] " + "which contains deprecated camel case to snake case. "
+        );
+        for (String pattern : patterns) {
+            LegacyFormatNames format = LegacyFormatNames.forName(pattern);
+            if (format != null && format.isCamelCase(pattern)) {
+                sb.append("[" + pattern + "] to [" + format.getSnakeCaseName() + "]. ");
+            }
+        }
+        sb.deleteCharAt(sb.length() - 1);
+        return sb.toString();
+    }
 }

+ 32 - 0
x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java

@@ -16,11 +16,13 @@ import org.elasticsearch.index.engine.frozen.FrozenEngine;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xpack.core.deprecation.DeprecationIssue;
 
+import java.io.IOException;
 import java.util.List;
 
 import static java.util.Collections.singletonList;
 import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS;
 import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
 
 public class IndexDeprecationChecksTests extends ESTestCase {
@@ -145,4 +147,34 @@ public class IndexDeprecationChecksTests extends ESTestCase {
             )
         );
     }
+
+    public void testCamelCaseDeprecation() throws IOException {
+        String simpleMapping = "{\n\"_doc\": {"
+            + "\"properties\" : {\n"
+            + "   \"date_time_field\" : {\n"
+            + "       \"type\" : \"date\",\n"
+            + "       \"format\" : \"strictDateOptionalTime\"\n"
+            + "       }\n"
+            + "   }"
+            + "} }";
+
+        IndexMetadata simpleIndex = IndexMetadata.builder(randomAlphaOfLengthBetween(5, 10))
+            .settings(settings(Version.V_7_0_0))
+            .numberOfShards(1)
+            .numberOfReplicas(1)
+            .putMapping(simpleMapping)
+            .build();
+
+        DeprecationIssue expected = new DeprecationIssue(
+            DeprecationIssue.Level.CRITICAL,
+            "Date fields use deprecated camel case formats",
+            "https://ela.st/es-deprecation-7-camel-case-format",
+            "Convert [date_time_field] format [strictDateOptionalTime] "
+                + "which contains deprecated camel case to snake case. [strictDateOptionalTime] to [strict_date_optional_time].",
+            false,
+            null
+        );
+        List<DeprecationIssue> issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(simpleIndex));
+        assertThat(issues, hasItem(expected));
+    }
 }