Skip to content
Open
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
205 changes: 202 additions & 3 deletions client-v2/src/main/java/com/clickhouse/client/api/DataTypeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader;
import com.clickhouse.data.ClickHouseDataType;

import java.math.BigInteger;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

import static com.clickhouse.client.api.data_formats.internal.BinaryStreamReader.BASES;

Expand Down Expand Up @@ -43,10 +48,16 @@ public class DataTypeUtils {
.appendFraction(ChronoField.NANO_OF_SECOND, 9, 9, true)
.toFormatter();

public static final DateTimeFormatter TIME_WITH_NANOS_FORMATTER = INSTANT_FORMATTER;
public static final DateTimeFormatter TIME_WITH_NANOS_FORMATTER = new DateTimeFormatterBuilder().appendPattern("HH:mm:ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.toFormatter();;

public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

public static final DateTimeFormatter DATE_TIME_WITH_OPTIONAL_NANOS = new DateTimeFormatterBuilder().appendPattern("uuuu-MM-dd HH:mm:ss")
.appendOptional(new DateTimeFormatterBuilder().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).toFormatter())
.toFormatter();

/**
* Formats an {@link Instant} object for use in SQL statements or as query
* parameter.
Expand Down Expand Up @@ -219,4 +230,192 @@ public static Duration localDateTimeToDuration(LocalDateTime localDateTime) {
return Duration.ofSeconds(localDateTime.toEpochSecond(ZoneOffset.UTC))
.plusNanos(localDateTime.getNano());
}


/**
* Converts a {@link java.sql.Date} to {@link LocalDate} using the specified timezone.
*
* <p>For default JVM timezone behavior, use {@link Date#toLocalDate()} directly.</p>
*
* @param sqlDate the java.sql.Date to convert
* @param timeZone the timezone context
* @return the LocalDate representing the date in the specified timezone
* @throws NullPointerException if sqlDate or timeZone is null
*/
public static LocalDate toLocalDate(Date sqlDate, TimeZone timeZone) {
Objects.requireNonNull(sqlDate, "sqlDate must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

ZoneId zoneId = timeZone.toZoneId();
return Instant.ofEpochMilli(sqlDate.getTime())
.atZone(zoneId)
.toLocalDate();
}

/**
* Converts a {@link java.sql.Time} to {@link LocalTime} using the specified timezone.
*
* <p>For default JVM timezone behavior, use {@link Time#toLocalTime()} directly.</p>
*
* @param sqlTime the java.sql.Time to convert
* @param timeZone the timezone context
* @return the LocalTime representing the time in the specified timezone
* @throws NullPointerException if sqlTime or timeZone is null
*/
public static LocalTime toLocalTime(Time sqlTime, TimeZone timeZone) {
Objects.requireNonNull(sqlTime, "sqlTime must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

ZoneId zoneId = timeZone.toZoneId();
return Instant.ofEpochMilli(sqlTime.getTime())
.atZone(zoneId)
.toLocalTime();
}

/**
* Converts a {@link java.sql.Timestamp} to {@link LocalDateTime} using the specified timezone.
*
* <p>Note: This method preserves nanosecond precision from the Timestamp.</p>
*
* <p>For default JVM timezone behavior, use {@link Timestamp#toLocalDateTime()} directly.</p>
*
* @param sqlTimestamp the java.sql.Timestamp to convert
* @param timeZone the timezone context
* @return the LocalDateTime representing the timestamp in the specified timezone
* @throws NullPointerException if sqlTimestamp or timeZone is null
*/
public static LocalDateTime toLocalDateTime(Timestamp sqlTimestamp, TimeZone timeZone) {
Objects.requireNonNull(sqlTimestamp, "sqlTimestamp must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

ZoneId zoneId = timeZone.toZoneId();
// Use Instant to preserve nanoseconds
return LocalDateTime.ofInstant(sqlTimestamp.toInstant(), zoneId);
}

/**
* Converts a {@link java.sql.Timestamp} to {@link ZonedDateTime} by expressing
* the timestamp's instant in the specified timezone.
*
* <p>The underlying instant is preserved — only the timezone context changes.
* This matches the JDBC {@code setTimestamp(int, Timestamp, Calendar)} contract
* where the Calendar's timezone is used to interpret the Timestamp's absolute
* point in time.</p>
*
* <p>Note: This method preserves nanosecond precision from the Timestamp.</p>
*
* @param sqlTimestamp the java.sql.Timestamp to convert
* @param timeZone the timezone to express the instant in
* @return the ZonedDateTime representing the same instant in the specified timezone
* @throws NullPointerException if sqlTimestamp or timeZone is null
*/
public static ZonedDateTime toZonedDateTime(Timestamp sqlTimestamp, TimeZone timeZone) {
Objects.requireNonNull(sqlTimestamp, "sqlTimestamp must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

return sqlTimestamp.toInstant().atZone(timeZone.toZoneId());
}

// ==================== LocalDate/LocalTime/LocalDateTime to SQL types ====================

/**
* Converts a {@link LocalDate} to {@link java.sql.Date} using the specified timezone.
*
* <p>For default JVM timezone behavior, use {@link Date#valueOf(LocalDate)} directly.</p>
*
* @param localDate the LocalDate to convert
* @param timeZone the timezone context
* @return the java.sql.Date representing midnight on the specified date in the given timezone
* @throws NullPointerException if localDate or timeZone is null
*/
public static Date toSqlDate(LocalDate localDate, TimeZone timeZone) {
Objects.requireNonNull(localDate, "localDate must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

long time = ZonedDateTime.of(localDate, LocalTime.MIDNIGHT, timeZone.toZoneId()).toEpochSecond() * 1000;
return new Date(time);
}

/**
* Converts a {@link LocalTime} to {@link java.sql.Time} using the specified timezone.
*
* <p>For default JVM timezone behavior, use {@link Time#valueOf(LocalTime)} directly.</p>
*
* @param localTime the LocalTime to convert
* @param timeZone the timezone context
* @return the java.sql.Time representing the specified time
* @throws NullPointerException if localTime or timeZone is null
*/
public static Time toSqlTime(LocalTime localTime, TimeZone timeZone) {
Objects.requireNonNull(localTime, "localTime must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

ZoneId zoneId = timeZone.toZoneId();
// java.sql.Time is based on January 1, 1970
long epochMillis = localTime.atDate(LocalDate.of(1970, 1, 1))
.atZone(zoneId)
.toInstant()
.toEpochMilli();
return new Time(epochMillis);
}


/**
* Converts a {@link LocalDateTime} to {@link java.sql.Timestamp} using the specified timezone.
*
* <p>Note: This method preserves nanosecond precision from the LocalDateTime.</p>
*
* <p>For default JVM timezone behavior, use {@link Timestamp#valueOf(LocalDateTime)} directly.</p>
*
* @param localDateTime the LocalDateTime to convert
* @param timeZone the timezone context
* @return the java.sql.Timestamp representing the specified date and time
* @throws NullPointerException if localDateTime or timeZone is null
*/
public static Timestamp toSqlTimestamp(LocalDateTime localDateTime, TimeZone timeZone) {
Objects.requireNonNull(localDateTime, "localDateTime must not be null");
Objects.requireNonNull(timeZone, "timeZone must not be null");

ZoneId zoneId = timeZone.toZoneId();
Instant instant = localDateTime.atZone(zoneId).toInstant();
Timestamp timestamp = Timestamp.from(instant);
// Timestamp.from() may lose nanosecond precision, so set it explicitly
timestamp.setNanos(localDateTime.getNano());
return timestamp;
}

private static final BigInteger NANOS_IN_SECOND = BigInteger.valueOf(1_000_000_000L);

// Max value of epoch second that can be converted to nanosecond without overflow (and fine to add Integer.MAX nanoseconds)
// Used to avoid BigInteger on small numbers
private static final long MAX_EPOCH_SECONDS_WITHOUT_OVERFLOW = (Long.MAX_VALUE - Integer.MAX_VALUE) / 1_000_000_000L;

public static String toUnixTimestampString(long seconds, int nanos) {
if (seconds <= MAX_EPOCH_SECONDS_WITHOUT_OVERFLOW) {
return String.valueOf(TimeUnit.SECONDS.toNanos(seconds) + nanos);
} else {
return BigInteger.valueOf(seconds).multiply(NANOS_IN_SECOND).add(BigInteger.valueOf(nanos)).toString();
}
}

/**
* Returns Unix Timestamp in nanoseconds as string
*
* @param localTs - LocalDateTime timestamp
* @param localTz - local timezone (useful to override default)
* @return String value.
*/
public static String toUnixTimestampString(LocalDateTime localTs, TimeZone localTz) {
return toUnixTimestampString(localTs.toEpochSecond(localTz.toZoneId().getRules().getOffset(localTs)), localTs.getNano());
}

/**
* Returns Unix Timestamp in nanoseconds as string
*
* @param instant - instant to convert
* @return String value.
*/
public static String toUnixTimestampString(Instant instant) {
return toUnixTimestampString(instant.getEpochSecond(), instant.getNano());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.clickhouse.client.api.ClientConfigProperties;
import com.clickhouse.client.api.ClientException;
import com.clickhouse.client.api.DataTypeUtils;
import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
import com.clickhouse.client.api.internal.DataTypeConverter;
import com.clickhouse.client.api.internal.MapUtils;
Expand Down Expand Up @@ -685,13 +686,31 @@ public ZonedDateTime getZonedDateTime(int index) {

@Override
public Duration getDuration(int index) {
TemporalAmount temporalAmount = getTemporalAmount(index);
return temporalAmount == null ? null : Duration.from(temporalAmount);
Object value = readValue(index);
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return DataTypeUtils.localDateTimeToDuration((LocalDateTime) value);
} else if (value instanceof TemporalAmount) {
return Duration.from((TemporalAmount)value);
}
throw new ClientException("Column at index " + index + " cannot be converted to Duration");
}

@Override
public TemporalAmount getTemporalAmount(int index) {
return readValue(index);
Object value = readValue(index);
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return DataTypeUtils.localDateTimeToDuration((LocalDateTime) value);
} else if (value instanceof TemporalAmount) {
return (TemporalAmount) value;
}

throw new ClientException("Column at index " + index + " cannot be converted to TemporalAmount");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.clickhouse.client.api.data_formats.internal;

import com.clickhouse.client.api.ClientException;
import com.clickhouse.client.api.DataTypeUtils;
import com.clickhouse.client.api.internal.DataTypeConverter;
import com.clickhouse.client.api.metadata.TableSchema;
import com.clickhouse.client.api.query.GenericRecord;
Expand Down Expand Up @@ -174,13 +175,31 @@ public ZonedDateTime getZonedDateTime(String colName) {

@Override
public Duration getDuration(String colName) {
TemporalAmount temporalAmount = readValue(colName);
return temporalAmount == null ? null : Duration.from(temporalAmount);
Object value = readValue(colName);
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return DataTypeUtils.localDateTimeToDuration((LocalDateTime) value);
} else if (value instanceof TemporalAmount) {
return Duration.from((TemporalAmount)value);
}
throw new ClientException("Column " + colName + " cannot be converted to Duration");
}

@Override
public TemporalAmount getTemporalAmount(String colName) {
return readValue(colName);
Object value = readValue(colName);
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return DataTypeUtils.localDateTimeToDuration((LocalDateTime) value);
} else if (value instanceof TemporalAmount) {
return (TemporalAmount) value;
}

throw new ClientException("Column " + colName + " cannot be converted to TemporalAmount");
}

@Override
Expand Down
Loading
Loading