diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java index 38849e5be..9c9d26a1b 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java @@ -56,6 +56,8 @@ public enum ClickHouseDataType { DateTime(LocalDateTime.class, true, false, false, 0, 29, 0, 0, 9, false, 0x11, "TIMESTAMP"), DateTime32(LocalDateTime.class, true, false, false, 4, 19, 0, 0, 0, false, 0x12), DateTime64(LocalDateTime.class, true, false, false, 8, 29, 3, 0, 9, false, 0x14), // we always write timezone as argument + Time(LocalTime.class, true, false, false, 4, 10, 0, 0, 0, false, 0x32), // 32-bit seconds since midnight + Time64(LocalTime.class, true, false, false, 8, 29, 3, 0, 9, false, 0x34), // 64-bit nanoseconds since midnight Enum(String.class, true, true, false, 0, 0, 0, 0, 0, false), Enum8(String.class, true, true, false, 1, 0, 0, 0, 0, false, 0x17, "ENUM"), Enum16(String.class, true, true, false, 2, 0, 0, 0, 0, false, 0x18), @@ -218,6 +220,8 @@ static Map>> dataTypeClassMap() { map.put(DateTime64, setOf(LocalDateTime.class, ZonedDateTime.class)); map.put(DateTime32, setOf(LocalDateTime.class, ZonedDateTime.class)); map.put(DateTime, setOf(LocalDateTime.class, ZonedDateTime.class)); + map.put(Time, setOf(LocalTime.class)); + map.put(Time64, setOf(LocalTime.class)); map.put(Enum8, setOf(java.lang.String.class,byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class, Long.class)); map.put(Enum16, setOf(java.lang.String.class,byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class, Long.class)); diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTime64Value.java b/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTime64Value.java new file mode 100644 index 000000000..66e6997b6 --- /dev/null +++ b/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTime64Value.java @@ -0,0 +1,290 @@ +package com.clickhouse.data.value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; +import com.clickhouse.data.ClickHouseChecker; +import com.clickhouse.data.ClickHouseDataConfig; +import com.clickhouse.data.ClickHouseValue; +import com.clickhouse.data.ClickHouseValues; + +/** + * Wrapper class of {@link LocalTime} for Time64 type (64-bit nanoseconds since midnight). + */ +public class ClickHouseTime64Value extends ClickHouseObjectValue { + /** + * Default value. + */ + public static final LocalTime DEFAULT = LocalTime.ofNanoOfDay(0L); + + static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.nnnnnnnnn"); + + /** + * Create a new instance representing null getValue(). + * + * @param scale scale (precision) + * @param tz time zone, null is treated as {@code UTC} + * @return new instance representing null value + */ + public static ClickHouseTime64Value ofNull(int scale, TimeZone tz) { + return ofNull(null, scale, tz); + } + + /** + * Update given value to null or create a new instance if {@code ref} is null. + * + * @param ref object to update, could be null + * @param scale scale (precision), only used when {@code ref} is null + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTime64Value ofNull(ClickHouseValue ref, int scale, TimeZone tz) { + return ref instanceof ClickHouseTime64Value + ? (ClickHouseTime64Value) ((ClickHouseTime64Value) ref).set(null) + : new ClickHouseTime64Value(null, scale, tz); + } + + /** + * Wrap the given getValue(). + * + * @param value value + * @param scale scale (precision) + * @param tz time zone, null is treated as {@code UTC} + * @return object representing the value + */ + public static ClickHouseTime64Value of(LocalTime value, int scale, TimeZone tz) { + return of(null, value, scale, tz); + } + + /** + * Wrap the given getValue(). + * + * @param value time in string format (HH:mm:ss.nnnnnnnnn) + * @param scale scale (precision) + * @param tz time zone, null is treated as {@code UTC} + * @return object representing the value + */ + public static ClickHouseTime64Value of(String value, int scale, TimeZone tz) { + return of(null, value, scale, tz); + } + + /** + * Update value of the given object or create a new instance if {@code ref} is + * null. + * + * @param ref object to update, could be null + * @param value value + * @param scale scale (precision), only used when {@code ref} is null + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTime64Value of(ClickHouseValue ref, LocalTime value, int scale, TimeZone tz) { + return ref instanceof ClickHouseTime64Value + ? (ClickHouseTime64Value) ((ClickHouseTime64Value) ref).set(value) + : new ClickHouseTime64Value(value, scale, tz); + } + + /** + * Update value of the given object or create a new instance if {@code ref} is + * null. + * + * @param ref object to update, could be null + * @param value time in string format (HH:mm:ss.nnnnnnnnn) + * @param scale scale (precision), only used when {@code ref} is null + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTime64Value of(ClickHouseValue ref, String value, int scale, TimeZone tz) { + LocalTime time = value == null || value.isEmpty() ? null + : LocalTime.parse(value, timeFormatter); + return of(ref, time, scale, tz); + } + + private final int scale; + private final TimeZone tz; + + protected ClickHouseTime64Value(LocalTime value, int scale, TimeZone tz) { + super(value); + this.scale = ClickHouseChecker.between(scale, ClickHouseValues.PARAM_SCALE, 0, 9); + this.tz = tz != null ? tz : ClickHouseValues.UTC_TIMEZONE; + } + + public int getScale() { + return scale; + } + + @Override + public ClickHouseTime64Value copy(boolean deep) { + return new ClickHouseTime64Value(getValue(), scale, tz); + } + + @Override + public byte asByte() { + return isNullOrEmpty() ? (byte) 0 : (byte) getValue().toNanoOfDay(); + } + + @Override + public short asShort() { + return isNullOrEmpty() ? (short) 0 : (short) getValue().toNanoOfDay(); + } + + @Override + public int asInteger() { + return isNullOrEmpty() ? 0 : (int) getValue().toNanoOfDay(); + } + + @Override + public long asLong() { + return isNullOrEmpty() ? 0L : getValue().toNanoOfDay(); + } + + @Override + public float asFloat() { + return isNullOrEmpty() ? 0F : getValue().toNanoOfDay(); + } + + @Override + public double asDouble() { + return isNullOrEmpty() ? 0D : getValue().toNanoOfDay(); + } + + @Override + public BigInteger asBigInteger() { + return isNullOrEmpty() ? null : BigInteger.valueOf(getValue().toNanoOfDay()); + } + + @Override + public BigDecimal asBigDecimal(int scale) { + LocalTime value = getValue(); + BigDecimal v = null; + if (value != null) { + long nanoSeconds = value.toNanoOfDay(); + v = new BigDecimal(BigInteger.valueOf(nanoSeconds), scale); + } + return v; + } + + @Override + public LocalTime asTime() { + return getValue(); + } + + @Override + public Object asObject() { + return getValue(); + } + + @Override + public String asString() { + LocalTime value = getValue(); + return value == null ? null : value.format(timeFormatter); + } + + @Override + public ClickHouseTime64Value resetToDefault() { + set(DEFAULT); + return this; + } + + @Override + public String toSqlExpression() { + LocalTime value = getValue(); + return value == null ? ClickHouseValues.NULL_EXPR : "'" + value.format(timeFormatter) + "'"; + } + + @Override + public ClickHouseTime64Value update(byte value) { + set(LocalTime.ofNanoOfDay(value)); + return this; + } + + @Override + public ClickHouseTime64Value update(short value) { + set(LocalTime.ofNanoOfDay(value)); + return this; + } + + @Override + public ClickHouseTime64Value update(int value) { + set(LocalTime.ofNanoOfDay(value)); + return this; + } + + @Override + public ClickHouseTime64Value update(long value) { + set(LocalTime.ofNanoOfDay(value)); + return this; + } + + @Override + public ClickHouseTime64Value update(float value) { + set(LocalTime.ofNanoOfDay((long) value)); + return this; + } + + @Override + public ClickHouseTime64Value update(double value) { + set(LocalTime.ofNanoOfDay((long) value)); + return this; + } + + @Override + public ClickHouseTime64Value update(BigInteger value) { + set(value == null ? null : LocalTime.ofNanoOfDay(value.longValue())); + return this; + } + + @Override + public ClickHouseTime64Value update(BigDecimal value) { + set(value == null ? null : LocalTime.ofNanoOfDay(value.longValue())); + return this; + } + + @Override + public ClickHouseTime64Value update(Enum value) { + set(value == null ? null : LocalTime.ofNanoOfDay(value.ordinal())); + return this; + } + + @Override + public ClickHouseTime64Value update(LocalTime value) { + set(value); + return this; + } + + @Override + public ClickHouseTime64Value update(String value) { + if (value == null || value.isEmpty()) { + set(null); + } else { + set(LocalTime.parse(value, timeFormatter)); + } + return this; + } + + @Override + public ClickHouseTime64Value update(ClickHouseValue value) { + if (value == null) { + set(null); + } else { + set(value.asTime()); + } + return this; + } + + @Override + public ClickHouseTime64Value update(Object value) { + if (value == null) { + set(null); + } else if (value instanceof LocalTime) { + set((LocalTime) value); + } else if (value instanceof Number) { + set(LocalTime.ofNanoOfDay(((Number) value).longValue())); + } else { + set(LocalTime.parse(value.toString(), timeFormatter)); + } + return this; + } +} \ No newline at end of file diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTimeValue.java b/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTimeValue.java new file mode 100644 index 000000000..047b0e0f1 --- /dev/null +++ b/clickhouse-data/src/main/java/com/clickhouse/data/value/ClickHouseTimeValue.java @@ -0,0 +1,272 @@ +package com.clickhouse.data.value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; +import com.clickhouse.data.ClickHouseChecker; +import com.clickhouse.data.ClickHouseDataConfig; +import com.clickhouse.data.ClickHouseValue; +import com.clickhouse.data.ClickHouseValues; + +/** + * Wrapper class of {@link LocalTime} for Time type (32-bit seconds since midnight). + */ +public class ClickHouseTimeValue extends ClickHouseObjectValue { + /** + * Default value. + */ + public static final LocalTime DEFAULT = LocalTime.ofSecondOfDay(0L); + + static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * Create a new instance representing null getValue(). + * + * @param tz time zone, null is treated as {@code UTC} + * @return new instance representing null value + */ + public static ClickHouseTimeValue ofNull(TimeZone tz) { + return ofNull(null, tz); + } + + /** + * Update given value to null or create a new instance if {@code ref} is null. + * + * @param ref object to update, could be null + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTimeValue ofNull(ClickHouseValue ref, TimeZone tz) { + return ref instanceof ClickHouseTimeValue + ? (ClickHouseTimeValue) ((ClickHouseTimeValue) ref).set(null) + : new ClickHouseTimeValue(null, tz); + } + + /** + * Wrap the given getValue(). + * + * @param value value + * @param tz time zone, null is treated as {@code UTC} + * @return object representing the value + */ + public static ClickHouseTimeValue of(LocalTime value, TimeZone tz) { + return of(null, value, tz); + } + + /** + * Wrap the given getValue(). + * + * @param value time in string format (HH:mm:ss) + * @param tz time zone, null is treated as {@code UTC} + * @return object representing the value + */ + public static ClickHouseTimeValue of(String value, TimeZone tz) { + return of(null, value, tz); + } + + /** + * Update value of the given object or create a new instance if {@code ref} is + * null. + * + * @param ref object to update, could be null + * @param value value + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTimeValue of(ClickHouseValue ref, LocalTime value, TimeZone tz) { + return ref instanceof ClickHouseTimeValue + ? (ClickHouseTimeValue) ((ClickHouseTimeValue) ref).set(value) + : new ClickHouseTimeValue(value, tz); + } + + /** + * Update value of the given object or create a new instance if {@code ref} is + * null. + * + * @param ref object to update, could be null + * @param value time in string format (HH:mm:ss) + * @param tz time zone, null is treated as {@code UTC} + * @return same object as {@code ref} or a new instance if it's null + */ + public static ClickHouseTimeValue of(ClickHouseValue ref, String value, TimeZone tz) { + LocalTime time = value == null || value.isEmpty() ? null + : LocalTime.parse(value, timeFormatter); + return of(ref, time, tz); + } + + private final TimeZone tz; + + protected ClickHouseTimeValue(LocalTime value, TimeZone tz) { + super(value); + this.tz = tz != null ? tz : ClickHouseValues.UTC_TIMEZONE; + } + + @Override + public ClickHouseTimeValue copy(boolean deep) { + return new ClickHouseTimeValue(getValue(), tz); + } + + @Override + public byte asByte() { + return isNullOrEmpty() ? (byte) 0 : (byte) getValue().toSecondOfDay(); + } + + @Override + public short asShort() { + return isNullOrEmpty() ? (short) 0 : (short) getValue().toSecondOfDay(); + } + + @Override + public int asInteger() { + return isNullOrEmpty() ? 0 : getValue().toSecondOfDay(); + } + + @Override + public long asLong() { + return isNullOrEmpty() ? 0L : getValue().toSecondOfDay(); + } + + @Override + public float asFloat() { + return isNullOrEmpty() ? 0F : getValue().toSecondOfDay(); + } + + @Override + public double asDouble() { + return isNullOrEmpty() ? 0D : getValue().toSecondOfDay(); + } + + @Override + public BigInteger asBigInteger() { + return isNullOrEmpty() ? null : BigInteger.valueOf(getValue().toSecondOfDay()); + } + + @Override + public BigDecimal asBigDecimal(int scale) { + return isNullOrEmpty() ? null : new BigDecimal(getValue().toSecondOfDay()); + } + + @Override + public LocalTime asTime() { + return getValue(); + } + + @Override + public Object asObject() { + return getValue(); + } + + @Override + public String asString() { + LocalTime value = getValue(); + return value == null ? null : value.format(timeFormatter); + } + + @Override + public ClickHouseTimeValue resetToDefault() { + set(DEFAULT); + return this; + } + + @Override + public String toSqlExpression() { + LocalTime value = getValue(); + return value == null ? ClickHouseValues.NULL_EXPR : "'" + value.format(timeFormatter) + "'"; + } + + @Override + public ClickHouseTimeValue update(byte value) { + set(LocalTime.ofSecondOfDay(value)); + return this; + } + + @Override + public ClickHouseTimeValue update(short value) { + set(LocalTime.ofSecondOfDay(value)); + return this; + } + + @Override + public ClickHouseTimeValue update(int value) { + set(LocalTime.ofSecondOfDay(value)); + return this; + } + + @Override + public ClickHouseTimeValue update(long value) { + set(LocalTime.ofSecondOfDay(value)); + return this; + } + + @Override + public ClickHouseTimeValue update(float value) { + set(LocalTime.ofSecondOfDay((long) value)); + return this; + } + + @Override + public ClickHouseTimeValue update(double value) { + set(LocalTime.ofSecondOfDay((long) value)); + return this; + } + + @Override + public ClickHouseTimeValue update(BigInteger value) { + set(value == null ? null : LocalTime.ofSecondOfDay(value.longValue())); + return this; + } + + @Override + public ClickHouseTimeValue update(BigDecimal value) { + set(value == null ? null : LocalTime.ofSecondOfDay(value.longValue())); + return this; + } + + @Override + public ClickHouseTimeValue update(Enum value) { + set(value == null ? null : LocalTime.ofSecondOfDay(value.ordinal())); + return this; + } + + @Override + public ClickHouseTimeValue update(LocalTime value) { + set(value); + return this; + } + + @Override + public ClickHouseTimeValue update(String value) { + if (value == null || value.isEmpty()) { + set(null); + } else { + set(LocalTime.parse(value, timeFormatter)); + } + return this; + } + + @Override + public ClickHouseTimeValue update(ClickHouseValue value) { + if (value == null) { + set(null); + } else { + set(value.asTime()); + } + return this; + } + + @Override + public ClickHouseTimeValue update(Object value) { + if (value == null) { + set(null); + } else if (value instanceof LocalTime) { + set((LocalTime) value); + } else if (value instanceof Number) { + set(LocalTime.ofSecondOfDay(((Number) value).longValue())); + } else { + set(LocalTime.parse(value.toString(), timeFormatter)); + } + return this; + } +} \ No newline at end of file diff --git a/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTime64ValueTest.java b/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTime64ValueTest.java new file mode 100644 index 000000000..0c5feafb3 --- /dev/null +++ b/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTime64ValueTest.java @@ -0,0 +1,138 @@ +package com.clickhouse.data.value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalTime; +import java.util.TimeZone; +import org.testng.Assert; +import org.testng.annotations.Test; +import com.clickhouse.data.BaseClickHouseValueTest; + +public class ClickHouseTime64ValueTest extends BaseClickHouseValueTest { + @Override + protected ClickHouseTime64Value create(LocalTime value) { + return ClickHouseTime64Value.of(value, 3, TimeZone.getDefault()); + } + + @Override + protected ClickHouseTime64Value createNull() { + return ClickHouseTime64Value.ofNull(3, TimeZone.getDefault()); + } + + @Override + protected LocalTime[] getTestValues() { + return new LocalTime[] { + LocalTime.ofNanoOfDay(0L), // 00:00:00.000000000 + LocalTime.ofNanoOfDay(3600_000_000_000L), // 01:00:00.000000000 + LocalTime.ofNanoOfDay(3661_123_000_000L), // 01:01:01.123000000 + LocalTime.ofNanoOfDay(86399_999_999_999L), // 23:59:59.999999999 + LocalTime.of(12, 34, 56, 123_456_789), // 12:34:56.123456789 + LocalTime.of(23, 59, 59, 999_999_999), // 23:59:59.999999999 + }; + } + + @Test(groups = { "unit" }) + public void testCreate() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + Assert.assertEquals(value.asTime(), LocalTime.of(12, 34, 56, 123_456_789)); + Assert.assertEquals(value.asString(), "12:34:56.123456789"); + } + + @Test(groups = { "unit" }) + public void testCreateFromString() { + ClickHouseTime64Value value = ClickHouseTime64Value.of("12:34:56.123456789", 3, TimeZone.getDefault()); + Assert.assertEquals(value.asTime(), LocalTime.of(12, 34, 56, 123_456_789)); + Assert.assertEquals(value.asString(), "12:34:56.123456789"); + } + + @Test(groups = { "unit" }) + public void testCreateNull() { + ClickHouseTime64Value value = createNull(); + Assert.assertNull(value.asTime()); + Assert.assertNull(value.asString()); + } + + @Test(groups = { "unit" }) + public void testUpdateFromNumber() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + + // Update from nanoseconds since midnight + value.update(3661_123_000_000L); // 01:01:01.123000000 + Assert.assertEquals(value.asTime(), LocalTime.of(1, 1, 1, 123_000_000)); + + value.update(3600_000_000_000L); // 01:00:00.000000000 + Assert.assertEquals(value.asTime(), LocalTime.of(1, 0, 0)); + } + + @Test(groups = { "unit" }) + public void testUpdateFromString() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + value.update("23:59:59.999999999"); + Assert.assertEquals(value.asTime(), LocalTime.of(23, 59, 59, 999_999_999)); + } + + @Test(groups = { "unit" }) + public void testUpdateFromLocalTime() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + value.update(LocalTime.of(23, 59, 59, 999_999_999)); + Assert.assertEquals(value.asTime(), LocalTime.of(23, 59, 59, 999_999_999)); + } + + @Test(groups = { "unit" }) + public void testAsNumber() { + ClickHouseTime64Value value = create(LocalTime.of(1, 1, 1, 123_000_000)); // 3661_123_000_000 nanoseconds + + Assert.assertEquals(value.asLong(), 3661_123_000_000L); + Assert.assertEquals(value.asInteger(), (int) 3661_123_000_000L); + Assert.assertEquals(value.asShort(), (short) 3661_123_000_000L); + Assert.assertEquals(value.asByte(), (byte) 3661_123_000_000L); + Assert.assertEquals(value.asFloat(), 3661_123_000_000.0f); + Assert.assertEquals(value.asDouble(), 3661_123_000_000.0); + } + + @Test(groups = { "unit" }) + public void testAsBigDecimal() { + ClickHouseTime64Value value = create(LocalTime.of(1, 1, 1, 123_000_000)); // 3661_123_000_000 nanoseconds + BigDecimal bd = value.asBigDecimal(0); + Assert.assertEquals(bd, new BigDecimal("3661123000000")); + } + + @Test(groups = { "unit" }) + public void testAsBigInteger() { + ClickHouseTime64Value value = create(LocalTime.of(1, 1, 1, 123_000_000)); // 3661_123_000_000 nanoseconds + BigInteger bi = value.asBigInteger(); + Assert.assertEquals(bi, BigInteger.valueOf(3661_123_000_000L)); + } + + @Test(groups = { "unit" }) + public void testToSqlExpression() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + Assert.assertEquals(value.toSqlExpression(), "'12:34:56.123456789'"); + + ClickHouseTime64Value nullValue = createNull(); + Assert.assertEquals(nullValue.toSqlExpression(), "NULL"); + } + + @Test(groups = { "unit" }) + public void testCopy() { + ClickHouseTime64Value original = create(LocalTime.of(12, 34, 56, 123_456_789)); + ClickHouseTime64Value copy = original.copy(false); + + Assert.assertEquals(copy.asTime(), original.asTime()); + Assert.assertEquals(copy.getScale(), original.getScale()); + Assert.assertNotSame(copy, original); + } + + @Test(groups = { "unit" }) + public void testResetToDefault() { + ClickHouseTime64Value value = create(LocalTime.of(12, 34, 56, 123_456_789)); + value.resetToDefault(); + Assert.assertEquals(value.asTime(), LocalTime.ofNanoOfDay(0L)); + } + + @Test(groups = { "unit" }) + public void testGetScale() { + ClickHouseTime64Value value = ClickHouseTime64Value.of(LocalTime.of(12, 34, 56), 5, TimeZone.getDefault()); + Assert.assertEquals(value.getScale(), 5); + } +} \ No newline at end of file diff --git a/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTimeValueTest.java b/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTimeValueTest.java new file mode 100644 index 000000000..fad2abcdd --- /dev/null +++ b/clickhouse-data/src/test/java/com/clickhouse/data/value/ClickHouseTimeValueTest.java @@ -0,0 +1,131 @@ +package com.clickhouse.data.value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalTime; +import java.util.TimeZone; +import org.testng.Assert; +import org.testng.annotations.Test; +import com.clickhouse.data.BaseClickHouseValueTest; + +public class ClickHouseTimeValueTest extends BaseClickHouseValueTest { + @Override + protected ClickHouseTimeValue create(LocalTime value) { + return ClickHouseTimeValue.of(value, TimeZone.getDefault()); + } + + @Override + protected ClickHouseTimeValue createNull() { + return ClickHouseTimeValue.ofNull(TimeZone.getDefault()); + } + + @Override + protected LocalTime[] getTestValues() { + return new LocalTime[] { + LocalTime.ofSecondOfDay(0L), // 00:00:00 + LocalTime.ofSecondOfDay(3600L), // 01:00:00 + LocalTime.ofSecondOfDay(3661L), // 01:01:01 + LocalTime.ofSecondOfDay(86399L), // 23:59:59 + LocalTime.of(12, 34, 56), // 12:34:56 + LocalTime.of(23, 59, 59), // 23:59:59 + }; + } + + @Test(groups = { "unit" }) + public void testCreate() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + Assert.assertEquals(value.asTime(), LocalTime.of(12, 34, 56)); + Assert.assertEquals(value.asString(), "12:34:56"); + } + + @Test(groups = { "unit" }) + public void testCreateFromString() { + ClickHouseTimeValue value = ClickHouseTimeValue.of("12:34:56", TimeZone.getDefault()); + Assert.assertEquals(value.asTime(), LocalTime.of(12, 34, 56)); + Assert.assertEquals(value.asString(), "12:34:56"); + } + + @Test(groups = { "unit" }) + public void testCreateNull() { + ClickHouseTimeValue value = createNull(); + Assert.assertNull(value.asTime()); + Assert.assertNull(value.asString()); + } + + @Test(groups = { "unit" }) + public void testUpdateFromNumber() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + + // Update from seconds since midnight + value.update(3661); // 01:01:01 + Assert.assertEquals(value.asTime(), LocalTime.of(1, 1, 1)); + + value.update(3600L); // 01:00:00 + Assert.assertEquals(value.asTime(), LocalTime.of(1, 0, 0)); + } + + @Test(groups = { "unit" }) + public void testUpdateFromString() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + value.update("23:59:59"); + Assert.assertEquals(value.asTime(), LocalTime.of(23, 59, 59)); + } + + @Test(groups = { "unit" }) + public void testUpdateFromLocalTime() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + value.update(LocalTime.of(23, 59, 59)); + Assert.assertEquals(value.asTime(), LocalTime.of(23, 59, 59)); + } + + @Test(groups = { "unit" }) + public void testAsNumber() { + ClickHouseTimeValue value = create(LocalTime.of(1, 1, 1)); // 3661 seconds + + Assert.assertEquals(value.asInteger(), 3661); + Assert.assertEquals(value.asLong(), 3661L); + Assert.assertEquals(value.asShort(), (short) 3661); + Assert.assertEquals(value.asByte(), (byte) 3661); + Assert.assertEquals(value.asFloat(), 3661.0f); + Assert.assertEquals(value.asDouble(), 3661.0); + } + + @Test(groups = { "unit" }) + public void testAsBigDecimal() { + ClickHouseTimeValue value = create(LocalTime.of(1, 1, 1)); // 3661 seconds + BigDecimal bd = value.asBigDecimal(0); + Assert.assertEquals(bd, new BigDecimal("3661")); + } + + @Test(groups = { "unit" }) + public void testAsBigInteger() { + ClickHouseTimeValue value = create(LocalTime.of(1, 1, 1)); // 3661 seconds + BigInteger bi = value.asBigInteger(); + Assert.assertEquals(bi, BigInteger.valueOf(3661)); + } + + @Test(groups = { "unit" }) + public void testToSqlExpression() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + Assert.assertEquals(value.toSqlExpression(), "'12:34:56'"); + + ClickHouseTimeValue nullValue = createNull(); + Assert.assertEquals(nullValue.toSqlExpression(), "NULL"); + } + + @Test(groups = { "unit" }) + public void testCopy() { + ClickHouseTimeValue original = create(LocalTime.of(12, 34, 56)); + ClickHouseTimeValue copy = original.copy(false); + + Assert.assertEquals(copy.asTime(), original.asTime()); + Assert.assertNotSame(copy, original); + } + + @Test(groups = { "unit" }) + public void testResetToDefault() { + ClickHouseTimeValue value = create(LocalTime.of(12, 34, 56)); + value.resetToDefault(); + Assert.assertEquals(value.asTime(), LocalTime.ofSecondOfDay(0L)); + } +} \ No newline at end of file diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java index 91da25ee8..ecc1cdee3 100644 --- a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java @@ -64,6 +64,11 @@ public static void main(String[] args) { jsonExample.writeData(); jsonExample.readData(); + // Demonstrate Time and Time64 types + TimeTypesExample timeExample = new TimeTypesExample(endpoint, user, password, database); + timeExample.demonstrateTimeTypes(); + timeExample.demonstrateTimeQueries(); + log.info("Done"); Runtime.getRuntime().exit(0); } diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/TimeTypesExample.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/TimeTypesExample.java new file mode 100644 index 000000000..415af7f80 --- /dev/null +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/TimeTypesExample.java @@ -0,0 +1,203 @@ +package com.clickhouse.examples.client_v2; + +import com.clickhouse.client.ClickHouseClient; +import com.clickhouse.client.ClickHouseConfig; +import com.clickhouse.client.ClickHouseNode; +import com.clickhouse.client.ClickHouseRequest; +import com.clickhouse.client.ClickHouseResponse; +import com.clickhouse.client.ClickHouseResponseSummary; +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.QueryResponse; +import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; +import com.clickhouse.client.api.data_formats.RowBinaryFormatSerializer; +import com.clickhouse.data.ClickHouseFormat; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.time.LocalTime; +import java.util.List; + +@Slf4j +public class TimeTypesExample { + private final String endpoint; + private final String user; + private final String password; + private final String database; + + public TimeTypesExample(String endpoint, String user, String password, String database) { + this.endpoint = endpoint; + this.user = user; + this.password = password; + this.database = database; + } + + public void demonstrateTimeTypes() { + log.info("=== Time Types Example ==="); + + // Create table with Time and Time64 columns + createTable(); + + // Insert data using binary format + insertTimeData(); + + // Read data back + readTimeData(); + + log.info("Time types example completed"); + } + + private void createTable() { + String createTableSql = String.format( + "CREATE TABLE IF NOT EXISTS %s.time_types_example (" + + "id UInt32, " + + "time_col Time, " + + "time64_col Time64(3), " + + "description String" + + ") ENGINE = MergeTree() ORDER BY id", + database + ); + + try (ClickHouseClient client = ClickHouseClient.builder() + .nodeSelector(ClickHouseNode.of(endpoint)) + .build(); + ClickHouseRequest request = client.read(createTableSql) + .format(ClickHouseFormat.RowBinary)) { + + request.executeAndWait(); + log.info("Table 'time_types_example' created successfully"); + } catch (Exception e) { + log.error("Failed to create table", e); + } + } + + private void insertTimeData() { + log.info("Inserting time data..."); + + try (ClickHouseClient client = ClickHouseClient.builder() + .nodeSelector(ClickHouseNode.of(endpoint)) + .build()) { + + // Prepare data + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + RowBinaryFormatSerializer serializer = new RowBinaryFormatSerializer(outputStream); + + // Sample data + LocalTime[] times = { + LocalTime.of(0, 0, 0), // 00:00:00 + LocalTime.of(12, 0, 0), // 12:00:00 + LocalTime.of(12, 34, 56), // 12:34:56 + LocalTime.of(23, 59, 59), // 23:59:59 + LocalTime.of(12, 34, 56, 123_456_789) // 12:34:56.123456789 + }; + + String[] descriptions = { + "Start of day", + "Noon", + "Specific time", + "End of day", + "Time with nanoseconds" + }; + + for (int i = 0; i < times.length; i++) { + serializer.writeUnsignedInt32(i + 1); // id + serializer.writeUnsignedInt32(times[i].toSecondOfDay()); // time_col (seconds since midnight) + serializer.writeUnsignedInt64(times[i].toNanoOfDay()); // time64_col (nanoseconds since midnight) + serializer.writeString(descriptions[i]); // description + } + + // Insert data + String insertSql = String.format( + "INSERT INTO %s.time_types_example (id, time_col, time64_col, description) VALUES", + database + ); + + try (ClickHouseRequest request = client.write(insertSql) + .format(ClickHouseFormat.RowBinary); + ClickHouseResponse response = request.data(new ByteArrayInputStream(outputStream.toByteArray())).executeAndWait()) { + + ClickHouseResponseSummary summary = response.getSummary(); + log.info("Inserted {} rows", summary.getWrittenRows()); + } + + } catch (Exception e) { + log.error("Failed to insert time data", e); + } + } + + private void readTimeData() { + log.info("Reading time data..."); + + String query = String.format( + "SELECT id, time_col, time64_col, description FROM %s.time_types_example ORDER BY id", + database + ); + + try (Client client = Client.builder() + .endpoint(endpoint) + .username(user) + .password(password) + .database(database) + .build()) { + + QueryResponse response = client.query(query).get(); + ClickHouseBinaryFormatReader reader = response.getBinaryReader(); + + while (reader.hasNext()) { + var record = reader.next(); + int id = record.getInt("id"); + LocalTime timeCol = record.getTime("time_col"); + LocalTime time64Col = record.getTime("time64_col"); + String description = record.getString("description"); + + log.info("Row {}: Time={}, Time64={}, Description={}", + id, timeCol, time64Col, description); + } + + } catch (Exception e) { + log.error("Failed to read time data", e); + } + } + + public void demonstrateTimeQueries() { + log.info("=== Time Queries Example ==="); + + String[] queries = { + // Query to find times after noon + "SELECT id, time_col, description FROM time_types_example WHERE time_col > 43200 ORDER BY time_col", + + // Query to find times with specific hour + "SELECT id, time_col, description FROM time_types_example WHERE toHour(time_col) = 12 ORDER BY time_col", + + // Query to format time in different ways + "SELECT id, time_col, toString(time_col) as time_str, description FROM time_types_example ORDER BY id", + + // Query to show time64 with precision + "SELECT id, time64_col, toString(time64_col) as time64_str, description FROM time_types_example ORDER BY id" + }; + + try (Client client = Client.builder() + .endpoint(endpoint) + .username(user) + .password(password) + .database(database) + .build()) { + + for (int i = 0; i < queries.length; i++) { + log.info("Query {}: {}", i + 1, queries[i]); + + QueryResponse response = client.query(queries[i]).get(); + ClickHouseBinaryFormatReader reader = response.getBinaryReader(); + + while (reader.hasNext()) { + var record = reader.next(); + log.info(" Result: {}", record); + } + log.info(""); + } + + } catch (Exception e) { + log.error("Failed to execute time queries", e); + } + } +} \ No newline at end of file