Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unknown camel case date-time mappings #88400

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/88400.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 88400
summary: Fix unknown camel case date-time mappings
area: Infra/Core
type: bug
issues:
- 84199
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public IndexMetadata verifyIndexMetadata(IndexMetadata indexMetadata, Version mi
// Next we have to run this otherwise if we try to create IndexSettings
// with broken settings it would fail in checkMappingsCompatibility
newMetadata = archiveBrokenIndexSettings(newMetadata);
checkMappingsCompatibility(newMetadata);
newMetadata = checkAndUpdateMappingsCompatibility(newMetadata);
return newMetadata;
}

Expand Down Expand Up @@ -118,7 +118,7 @@ private static void checkSupportedVersion(IndexMetadata indexMetadata, Version m
}

/**
* Check that we can parse the mappings.
* Check that we can parse the mappings and update mapping metadata if needed.
*
* This is not strictly necessary, since we parse the mappings later when loading the index and will
* catch issues then. But it lets us fail very quickly and clearly: if there is a mapping incompatibility,
Expand All @@ -128,7 +128,7 @@ private static void checkSupportedVersion(IndexMetadata indexMetadata, Version m
* policy guarantees we can read mappings from previous compatible index versions. A failure here would
* indicate a compatibility bug (which are unfortunately not that uncommon).
*/
private void checkMappingsCompatibility(IndexMetadata indexMetadata) {
private IndexMetadata checkAndUpdateMappingsCompatibility(IndexMetadata indexMetadata) {
try {

// We cannot instantiate real analysis server or similarity service at this point because the node
Expand Down Expand Up @@ -194,12 +194,14 @@ public Set<Entry<String, NamedAnalyzer>> entrySet() {
indexSettings.getMode().idFieldMapperWithoutFieldData(),
scriptService
);
mapperService.merge(indexMetadata, MapperService.MergeReason.MAPPING_RECOVERY);
indexMetadata = mapperService.mergeAndUpgrade(indexMetadata, MapperService.MergeReason.MAPPING_RECOVERY);
}
} catch (Exception ex) {
// Wrap the inner exception so we have the index name in the exception message
throw new IllegalStateException("Failed to parse mappings for index [" + indexMetadata.getIndex() + "]", ex);
}

return indexMetadata;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied back from 7.17, with removed unnecessary methods.

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 String camelCaseToSnakeCase(String format) {
return ALL_NAMES.getOrDefault(format, format);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import org.elasticsearch.common.time.DateFormatters;
import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.time.DateUtils;
import org.elasticsearch.common.time.LegacyFormatNames;
import org.elasticsearch.common.util.LocaleUtils;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.fielddata.FieldDataContext;
Expand Down Expand Up @@ -269,19 +271,32 @@ public Builder(
DateFormatter defaultFormat = resolution == Resolution.MILLISECONDS
? DEFAULT_DATE_TIME_FORMATTER
: DEFAULT_DATE_TIME_NANOS_FORMATTER;
this.format = Parameter.stringParam(
"format",
indexCreatedVersion.isLegacyIndexVersion(),
m -> toType(m).format,
defaultFormat.pattern()
);

if (indexCreatedVersion.major <= Version.V_8_0_0.previousMajor().major) {
this.format = Parameter.stringParam(
"format",
indexCreatedVersion.isLegacyIndexVersion(),
(n, c, o) -> LegacyFormatNames.camelCaseToSnakeCase(XContentMapValues.nodeStringValue(o)),
m -> toType(m).format,
defaultFormat.pattern(),
XContentBuilder::field
);
} else {
this.format = Parameter.stringParam(
"format",
indexCreatedVersion.isLegacyIndexVersion(),
m -> toType(m).format,
defaultFormat.pattern()
);
}
if (dateFormatter != null) {
this.format.setValue(dateFormatter.pattern());
this.locale.setValue(dateFormatter.locale());
}
}

private DateFormatter buildFormatter() {
// package private for testing
DateFormatter buildFormatter() {
try {
return DateFormatter.forPattern(format.getValue()).withLocale(locale.getValue());
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.time.LegacyFormatNames;
import org.elasticsearch.common.util.LocaleUtils;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.fielddata.DateScriptFieldData;
Expand Down Expand Up @@ -51,6 +53,7 @@ private static class Builder extends AbstractScriptFieldType.Builder<DateFieldSc
private final FieldMapper.Parameter<String> format = FieldMapper.Parameter.stringParam(
"format",
true,
(n, c, o) -> LegacyFormatNames.camelCaseToSnakeCase(XContentMapValues.nodeStringValue(o)),
RuntimeField.initializerNotSupported(),
null,
(b, n, v) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -878,12 +878,23 @@ public static Parameter<String> stringParam(
Function<FieldMapper, String> initializer,
String defaultValue,
Serializer<String> serializer
) {
return stringParam(name, updateable, (n, c, o) -> XContentMapValues.nodeStringValue(o), initializer, defaultValue, serializer);
}

public static Parameter<String> stringParam(
String name,
boolean updateable,
TriFunction<String, MappingParserContext, Object, String> parser,
Function<FieldMapper, String> initializer,
String defaultValue,
Serializer<String> serializer
) {
return new Parameter<>(
name,
updateable,
defaultValue == null ? () -> null : () -> defaultValue,
(n, c, o) -> XContentMapValues.nodeStringValue(o),
parser,
initializer,
serializer,
Function.identity()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,23 @@ public void merge(IndexMetadata indexMetadata, MergeReason reason) {
}
}

public IndexMetadata mergeAndUpgrade(IndexMetadata indexMetadata, MergeReason reason) {
assert reason != MergeReason.MAPPING_UPDATE_PREFLIGHT;
MappingMetadata mappingMetadata = indexMetadata.mapping();
if (mappingMetadata != null) {
var docMapper = merge(mappingMetadata.type(), mappingMetadata.source(), reason);
if (indexVersionCreated.major <= Version.V_8_0_0.previousMajor().major) {
var upgradedSource = MapperServiceUpgraders.upgradeDateFormatsIfNeeded(mappingMetadata, docMapper.mappingSource());

// reference equality for performance purposes
if (mappingMetadata.source() != upgradedSource) {
return IndexMetadata.builder(indexMetadata).putMapping(new MappingMetadata(upgradedSource)).build();
}
}
}
return indexMetadata;
}

public DocumentMapper merge(String type, CompressedXContent mappingSource, MergeReason reason) {
final DocumentMapper currentMapper = this.mapper;
if (currentMapper != null && currentMapper.mappingSource().equals(mappingSource)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.cluster.metadata.MappingMetadata;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.xcontent.XContentHelper;

import java.io.IOException;
import java.util.Map;

/**
* Contains a collection of helper methods for index mapping upgrades
*/
public class MapperServiceUpgraders {
@SuppressWarnings("unchecked")
private static Map<String, Object> getMappingsForType(Map<String, Object> mappingMap, String type) {
// Check for MappingMap without type and ignore it
if (mappingMap.size() != 1) {
return null;
}
// Fetch the mappings for the encoded type
return (Map<String, Object>) mappingMap.get(type);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private static boolean upgradeDateFormatProperties(Map<String, Object> properties, Map<String, Object> parsedProperties) {
boolean anyChanges = false;

if (properties != null && parsedProperties != null) {
for (var property : properties.entrySet()) {
if (property.getValue()instanceof Map map) {
var parsedProperty = parsedProperties.get(property.getKey());
if (parsedProperty instanceof Map parsedMap) {
Object format = map.get("format");
if (format != null) {
Object parsedFormat = parsedMap.get("format");
if (parsedFormat != null && parsedFormat.equals(format) == false) {
map.put("format", parsedFormat);
anyChanges = true;
}
} else {
var anyNestedChanges = upgradeDateFormatProperties(map, parsedMap);
anyChanges = anyChanges || anyNestedChanges;
}
}
}
}
}

return anyChanges;
}

/**
* Upgrades the date format field of mapping properties
* <p>
* This method checks if the new mapping source has a different format than the original mapping, and if it
* does, then it returns the original mapping source with updated format field. We cannot simply replace the
* MappingMetadata mappingSource with the new parsedSource, because the parsed source contains additional
* properties which cannot be serialized and reread.
*
* @param mappingMetadata the current mapping metadata
* @param parsedSource the mapping source as it was parsed
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static CompressedXContent upgradeDateFormatsIfNeeded(MappingMetadata mappingMetadata, CompressedXContent parsedSource) {
Map<String, Object> sourceMap = mappingMetadata.rawSourceAsMap();
Map<String, Object> newMap = XContentHelper.convertToMap(parsedSource.compressedReference(), true).v2();

Map<String, Object> mappings = getMappingsForType(sourceMap, mappingMetadata.type());
Map<String, Object> parsedMappings = getMappingsForType(newMap, mappingMetadata.type());

if (mappings == null || parsedMappings == null) {
return mappingMetadata.source();
}

boolean anyPropertiesChanges = upgradeDateFormatProperties(
(Map<String, Object>) mappings.get("properties"),
(Map<String, Object>) parsedMappings.get("properties")
);

boolean anyRuntimeChanges = upgradeDateFormatProperties(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't do anything here for dynamic_templates, they don't seem to trigger a parse of a Date field in the same way it's happening for 'properties' and 'runtime'.

(Map<String, Object>) mappings.get("runtime"),
(Map<String, Object>) parsedMappings.get("runtime")
);

if (anyRuntimeChanges == false && anyPropertiesChanges == false) {
return mappingMetadata.source();
}

try {
return new CompressedXContent(sourceMap);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you keep track of whether any change was made above, so that the original parsedSource can be returned here and reference equality could be used by the caller to detect changes? That would make it less expensive when there aren't any changes (which is most of the time).

} catch (IOException e) {
throw new IllegalStateException("Unexpected error remapping source map", e);
}
}
}
Loading