All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.debezium.connector.mysql.converters.MysqlDebeziumTimeConverter Maven / Gradle / Ivy

There is a newer version: 3.3.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.debezium.connector.mysql.converters;

import org.apache.flink.cdc.debezium.utils.ConvertTimeBceUtil;

import io.debezium.spi.converter.CustomConverter;
import io.debezium.spi.converter.RelationalColumn;
import io.debezium.time.Conversions;
import org.apache.kafka.connect.data.SchemaBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

/**
 * Debezium converts the datetime type in MySQL into a UTC timestamp by default ({@link
 * io.debezium.time.Timestamp} ),The time zone is hard-coded and cannot be changed. causing
 * conversion errors part of the time Enable this converter to convert the four times "DATE",
 * "DATETIME", "TIME", and "TIMESTAMP" into the format corresponding to the configured time zone
 * (for example, yyyy-MM-dd)
 *
 * @see io.debezium.connector.mysql.converters.TinyIntOneToBooleanConverter
 */
public class MysqlDebeziumTimeConverter
        implements CustomConverter {

    private static final Logger log = LoggerFactory.getLogger(MysqlDebeziumTimeConverter.class);

    private static boolean loggedUnknownTimestampClass = false;
    private static boolean loggedUnknownDateClass = false;
    private static boolean loggedUnknownTimeClass = false;
    private static boolean loggedUnknownTimestampWithTimeZoneClass = false;

    private final String DATE = "DATE";
    private final String DATETIME = "DATETIME";
    private final String TIME = "TIME";
    private final String TIMESTAMP = "TIMESTAMP";
    private final String[] DATE_TYPES = {DATE, DATETIME, TIME, TIMESTAMP};

    protected static final String DATE_FORMAT = "yyyy-MM-dd";
    protected static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    protected static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    protected ZoneId zoneId;
    protected static final String DEFAULT_DATE_FORMAT_PATTERN = "1970-01-01 00:00:00";
    protected DateTimeFormatter dateFormatter;
    protected DateTimeFormatter timeFormatter;
    protected DateTimeFormatter datetimeFormatter;
    protected DateTimeFormatter timestampFormatter;
    protected String schemaNamePrefix;
    protected static DateTimeFormatter originalFormat =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    protected Boolean parseNullDefaultValue = true;

    @Override
    public void configure(Properties properties) {
        String dateFormat = properties.getProperty("format.date", DATE_FORMAT);
        String timeFormat = properties.getProperty("format.time", TIME_FORMAT);
        String datetimeFormat = properties.getProperty("format.datetime", DATETIME_FORMAT);
        String timestampFormat = properties.getProperty("format.timestamp", DATETIME_FORMAT);
        this.parseNullDefaultValue =
                Boolean.parseBoolean(
                        properties.getProperty("format.default.value.convert", "true"));
        String className = this.getClass().getName();
        this.schemaNamePrefix = properties.getProperty("schema.name.prefix", className + ".mysql");
        this.dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
        this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat);
        this.datetimeFormatter = DateTimeFormatter.ofPattern(datetimeFormat);
        this.timestampFormatter = DateTimeFormatter.ofPattern(timestampFormat);
        this.zoneId =
                ZoneId.of(
                        properties.getProperty(
                                "format.timezone", ZoneId.systemDefault().toString()));
    }

    @Override
    public void converterFor(
            final RelationalColumn field, final ConverterRegistration registration) {
        if (Arrays.stream(DATE_TYPES).anyMatch(s -> s.equalsIgnoreCase(field.typeName()))) {
            registerDateConverter(field, registration);
        }
    }

    private void registerDateConverter(
            final RelationalColumn field, final ConverterRegistration registration) {
        String columnType = field.typeName().toUpperCase();
        String schemaName = this.schemaNamePrefix + "." + columnType.toLowerCase();
        registration.register(
                SchemaBuilder.string().name(schemaName).optional(),
                value -> {
                    log.debug(
                            "find schema need to change dateType, field name:{} ,field type:{} ,field value:{} ,field "
                                    + "default:{}",
                            field.name(),
                            columnType,
                            value == null ? "null" : value,
                            field.hasDefaultValue() ? field.defaultValue() : "null");
                    if (value == null) {
                        return convertDateDefaultValue(field);
                    }
                    switch (columnType.toUpperCase(Locale.ROOT)) {
                        case DATE:
                            if (value instanceof Integer) {
                                return this.convertToDate(
                                        columnType, LocalDate.ofEpochDay((Integer) value));
                            }
                            return this.convertToDate(columnType, value);
                        case TIME:
                            if (value instanceof Long) {
                                long l =
                                        Math.multiplyExact(
                                                (Long) value, TimeUnit.MICROSECONDS.toNanos(1));
                                return this.convertToTime(columnType, LocalTime.ofNanoOfDay(l));
                            }
                            return this.convertToTime(columnType, value);
                        case DATETIME:
                            if (value instanceof Long) {
                                if (getTimePrecision(field) <= 3) {
                                    return this.convertToTimestamp(
                                            columnType,
                                            Conversions.toInstantFromMillis((Long) value));
                                }
                                if (getTimePrecision(field) <= 6) {
                                    return this.convertToTimestamp(
                                            columnType,
                                            Conversions.toInstantFromMicros((Long) value));
                                }
                            }
                            return this.convertToTimestamp(columnType, value);
                        case TIMESTAMP:
                            return this.convertToTimestampWithTimezone(columnType, value);
                        default:
                            throw new IllegalArgumentException(
                                    "Unknown field type  " + columnType.toUpperCase(Locale.ROOT));
                    }
                });
    }

    private Object convertToTimestampWithTimezone(String columnType, Object timestamp) {
        // In snapshot mode, debezium produces a java.sql.Timestamp object for the TIMESTAMPTZ type.
        // Conceptually, a timestamp with timezone is an Instant. But t.toInstant() actually
        // mangles the value for ancient dates, because leap years weren't applied consistently in
        // ye olden days. Additionally, toInstant() (and toLocalDateTime()) actually lose the era
        // indicator,
        // so we can't rely on their getEra() methods.
        // So we have special handling for this case, which sidesteps the toInstant conversion.
        if (timestamp instanceof Timestamp) {
            Timestamp value = (Timestamp) timestamp;
            ZonedDateTime zonedDateTime = value.toInstant().atZone(zoneId);
            return ConvertTimeBceUtil.resolveEra(value, zonedDateTime.format(timestampFormatter));
        } else if (timestamp instanceof OffsetDateTime) {
            OffsetDateTime value = (OffsetDateTime) timestamp;
            return ConvertTimeBceUtil.resolveEra(
                    value.toLocalDate(), value.format(timestampFormatter));
        } else if (timestamp instanceof ZonedDateTime) {
            ZonedDateTime zonedDateTime = (ZonedDateTime) timestamp;
            return ConvertTimeBceUtil.resolveEra(
                    zonedDateTime.toLocalDate(), zonedDateTime.format(timestampFormatter));
        } else if (timestamp instanceof Instant) {
            OffsetDateTime dateTime = OffsetDateTime.ofInstant((Instant) timestamp, zoneId);
            ZonedDateTime timestampZt = ZonedDateTime.from(dateTime);
            LocalDate localDate = timestampZt.toLocalDate();
            return ConvertTimeBceUtil.resolveEra(localDate, timestampZt.format(timestampFormatter));
        } else {
            if (!loggedUnknownTimestampWithTimeZoneClass) {
                printUnknownDateClassLogs(columnType, timestamp);
                loggedUnknownTimestampWithTimeZoneClass = true;
            }
            // If init 1970-01-01T00:00:00Zd need to change
            Instant instant = Instant.parse(timestamp.toString());
            OffsetDateTime dateTime = OffsetDateTime.ofInstant(instant, zoneId);
            ZonedDateTime timestampZt = ZonedDateTime.from(dateTime);
            LocalDate localDate = timestampZt.toLocalDate();
            return ConvertTimeBceUtil.resolveEra(localDate, timestampZt.format(timestampFormatter));
        }
    }

    private Object convertToTimestamp(String columnType, Object timestamp) {
        if (timestamp instanceof Timestamp) {
            // Snapshot mode
            LocalDateTime localDateTime = ((Timestamp) timestamp).toLocalDateTime();
            return ConvertTimeBceUtil.resolveEra(
                    ((Timestamp) timestamp), localDateTime.format(datetimeFormatter));
        } else if (timestamp instanceof Instant) {
            // Incremental mode
            Instant time = (Instant) timestamp;
            ZonedDateTime zonedDateTime = time.atZone(zoneId);
            return ConvertTimeBceUtil.resolveEra(
                    zonedDateTime.toLocalDate(),
                    time.atOffset(zonedDateTime.getOffset())
                            .toLocalDateTime()
                            .format(datetimeFormatter));
        } else if (timestamp instanceof LocalDateTime) {
            LocalDateTime dateTime = (LocalDateTime) timestamp;
            LocalDate localDateTime = dateTime.toLocalDate();
            return ConvertTimeBceUtil.resolveEra(localDateTime, dateTime.format(datetimeFormatter));
        }
        if (!loggedUnknownTimestampClass) {
            printUnknownDateClassLogs(columnType, timestamp);
            loggedUnknownTimestampClass = true;
        }
        LocalDateTime localDateTime = LocalDateTime.parse(timestamp.toString());
        LocalDate localDate = localDateTime.toLocalDate();
        return ConvertTimeBceUtil.resolveEra(localDate, localDateTime.format(datetimeFormatter));
    }

    private Object convertToTime(String columnType, Object time) {
        if (time instanceof Time) {
            return formatTime(((Time) time).toLocalTime());
        } else if (time instanceof LocalTime) {
            return formatTime((LocalTime) time);
        } else if (time instanceof java.time.Duration) {
            long value = ((java.time.Duration) time).toNanos();
            if (value >= 0 && value < TimeUnit.DAYS.toNanos(1)) {
                return formatTime(LocalTime.ofNanoOfDay(value));
            } else {
                long updatedValue = Math.min(Math.abs(value), LocalTime.MAX.toNanoOfDay());
                log.debug(
                        "Time values must use number of nanoseconds greater than 0 and less than 86400000000000 but its {}, "
                                + "converting to {} ",
                        value,
                        updatedValue);
                return formatTime(LocalTime.ofNanoOfDay(updatedValue));
            }
        } else {
            if (!loggedUnknownTimeClass) {
                printUnknownDateClassLogs(columnType, time);
                loggedUnknownTimeClass = true;
            }

            String valueAsString = time.toString();
            if (valueAsString.startsWith("24")) {
                log.debug("Time value {} is above range, converting to 23:59:59", valueAsString);
                return LocalTime.MAX.toString();
            }
            return formatTime(LocalTime.parse(valueAsString));
        }
    }

    private String formatTime(LocalTime localTime) {
        return localTime.format(timeFormatter);
    }

    private int getTimePrecision(final RelationalColumn field) {
        return field.length().orElse(-1);
    }

    private String convertToDate(String columnType, Object date) {
        if (date instanceof Date) {
            // Snapshot mode
            LocalDate localDate = ((Date) date).toLocalDate();
            return ConvertTimeBceUtil.resolveEra(localDate, localDate.format(dateFormatter));
        } else if (date instanceof LocalDate) {
            // Incremental mode
            return dateFormatter.format((LocalDate) date);
        } else if (date instanceof LocalDateTime) {
            return datetimeFormatter.format((LocalDateTime) date);
        } else if (date instanceof Integer) {
            return LocalDate.ofEpochDay(((Integer) date).longValue()).format(dateFormatter);
        } else {
            if (!loggedUnknownDateClass) {
                printUnknownDateClassLogs(columnType, date);
                loggedUnknownDateClass = true;
            }
            LocalDate localDate = LocalDate.parse(date.toString());
            return ConvertTimeBceUtil.resolveEra(localDate, localDate.format(dateFormatter));
        }
    }

    public Object convertDateDefaultValue(RelationalColumn field) {
        if (field.isOptional()) {
            return null;
        } else if (field.hasDefaultValue()) {
            // There is an extreme case where the field defaultValue is 0, resulting in a Kafka
            // Schema mismatch
            if (parseNullDefaultValue) {
                LocalDateTime dateTime =
                        LocalDateTime.parse(DEFAULT_DATE_FORMAT_PATTERN, originalFormat);
                String columnType = field.typeName().toUpperCase();
                switch (columnType.toUpperCase(Locale.ROOT)) {
                    case DATE:
                        return dateTime.format(dateFormatter);
                    case DATETIME:
                        return dateTime.format(datetimeFormatter);
                    case TIME:
                        return dateTime.format(timeFormatter);
                    case TIMESTAMP:
                        return dateTime.format(timestampFormatter);
                }
            }
        }
        return null;
    }

    private static void printUnknownDateClassLogs(String type, Object value) {
        log.warn(
                "MySql Date Convert Database type : {} Unknown class for Date data type {}",
                type,
                value.getClass());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy