You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by mb...@apache.org on 2026/09/08 07:03:34 UTC
(calcite-avatica) 01/01: [CALCITE-7494] Avatica conversion to string of TIMESTAMP WITH TIME ZONE does not include time zone
This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch issue7494
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git
commit c2ae6233184a4f9f0df11fa15b9e5de320447a4d
Author: Mihai Budiu <mb...@feldera.com>
AuthorDate: Tue Aug 4 11:28:18 2026 -0700
[CALCITE-7494] Avatica conversion to string of TIMESTAMP WITH TIME ZONE does not include time zone
Signed-off-by: Mihai Budiu <mb...@feldera.com>
---
.../calcite/avatica/util/AbstractCursor.java | 36 +++++
.../util/TimestampTzFromNumberAccessorTest.java | 152 +++++++++++++++++++++
2 files changed, 188 insertions(+)
diff --git a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
index e63ac7172..47ffe8c67 100644
--- a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
+++ b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
@@ -170,6 +170,10 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
case PRIMITIVE_LONG:
case LONG:
case NUMBER:
+ if (isTimestampTz(columnMetaData)) {
+ return new TimestampTzFromNumberAccessor(getter, localCalendar,
+ columnMetaData.precision);
+ }
return new TimestampFromNumberAccessor(getter, localCalendar, columnMetaData.precision);
case JAVA_SQL_TIMESTAMP:
return new TimestampAccessor(getter, localCalendar, columnMetaData.precision);
@@ -239,6 +243,17 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
}
}
+ /** Returns whether a column contains TIMESTAMP WITH TIME ZONE values.
+ *
+ * <p>Such columns share the JDBC type {@link Types#TIMESTAMP} with plain
+ * TIMESTAMP columns; only the type name distinguishes them. */
+ private static boolean isTimestampTz(ColumnMetaData columnMetaData) {
+ final String typeName = columnMetaData.type.getName();
+ return typeName != null
+ && (typeName.startsWith("TIMESTAMP_TZ")
+ || typeName.startsWith("TIMESTAMP WITH TIME ZONE"));
+ }
+
protected abstract Getter createGetter(int ordinal);
public abstract boolean next();
@@ -1183,6 +1198,27 @@ protected Number getNumber() throws SQLException {
}
}
+ /**
+ * Accessor that assumes that the underlying value is a TIMESTAMP WITH TIME
+ * ZONE, in its default representation {@code long}: milliseconds since the
+ * UNIX epoch in UTC; corresponds to a {@link java.sql.Types#TIMESTAMP}
+ * column whose type name is {@code TIMESTAMP_TZ}.
+ */
+ static class TimestampTzFromNumberAccessor extends TimestampFromNumberAccessor {
+ TimestampTzFromNumberAccessor(Getter getter, Calendar localCalendar, int precision) {
+ super(getter, localCalendar, precision);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The value is rendered in UTC and includes the time zone,
+ * e.g. "1970-01-01 00:00:00 UTC". */
+ @Override public String getString() throws SQLException {
+ final String s = super.getString();
+ return s == null ? null : s + " " + DateTimeUtils.UTC_ZONE.getID();
+ }
+ }
+
/**
* Accessor that assumes that the underlying value is a DATE,
* represented as a java.sql.Date;
diff --git a/core/src/test/java/org/apache/calcite/avatica/util/TimestampTzFromNumberAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimestampTzFromNumberAccessorTest.java
new file mode 100644
index 000000000..90900e538
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampTzFromNumberAccessorTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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 org.apache.calcite.avatica.util;
+
+import org.apache.calcite.avatica.ColumnMetaData;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * <p>Test cases for
+ * <a href="https://issues.apache.org/jira/browse/CALCITE-7494">[CALCITE-7494]
+ * Avatica conversion to string of TIMESTAMP WITH TIME ZONE does not include
+ * time zone</a>.
+ */
+public class TimestampTzFromNumberAccessorTest {
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+
+ private Cursor.Accessor instance;
+ private Calendar localCalendar;
+ private Object value;
+
+ /**
+ * Setup test environment by creating a
+ * {@link AbstractCursor.TimestampTzFromNumberAccessor} that reads from the
+ * instance variable {@code value}.
+ */
+ @Before public void before() {
+ final AbstractCursor.Getter getter = new LocalGetter();
+ localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+ instance =
+ new AbstractCursor.TimestampTzFromNumberAccessor(getter, localCalendar, 0);
+ }
+
+ /**
+ * Test {@code getString()} includes the time zone.
+ */
+ @Test public void testString() throws SQLException {
+ value = 0L;
+ assertThat(instance.getString(), is("1970-01-01 00:00:00 UTC"));
+
+ value = DST_INSTANT;
+ assertThat(instance.getString(), is("2014-09-30 15:28:27 UTC"));
+ }
+
+ /**
+ * Test {@code getString()} keeps the fractional seconds allowed by the
+ * column precision; fractional seconds beyond the precision are truncated,
+ * not rounded.
+ */
+ @Test public void testStringWithPrecision() throws SQLException {
+ value = DST_INSTANT;
+ assertThat(stringWithPrecision(3), is("2014-09-30 15:28:27.356 UTC"));
+ assertThat(stringWithPrecision(2), is("2014-09-30 15:28:27.35 UTC"));
+ assertThat(stringWithPrecision(1), is("2014-09-30 15:28:27.3 UTC"));
+ assertThat(stringWithPrecision(0), is("2014-09-30 15:28:27 UTC"));
+ }
+
+ /** Renders {@code value} via an accessor with the given column precision. */
+ private String stringWithPrecision(int precision) throws SQLException {
+ return new AbstractCursor.TimestampTzFromNumberAccessor(new LocalGetter(),
+ null, precision).getString();
+ }
+
+ /**
+ * Test {@code getString()} returns null for a null value.
+ */
+ @Test public void testStringNull() throws SQLException {
+ value = null;
+ assertThat(instance.getString(), nullValue());
+ }
+
+ /**
+ * Test {@code getTimestamp()} is not affected by the time zone suffix.
+ */
+ @Test public void testTimestamp() throws SQLException {
+ value = DateTimeUtils.timestampStringToUnixDate("2014-09-30 15:28:27.356");
+ assertThat(instance.getTimestamp(localCalendar),
+ is(Timestamp.valueOf("2014-09-30 15:28:27.356")));
+ }
+
+ /**
+ * Test a column whose JDBC type is {@link Types#TIMESTAMP} but whose type
+ * name is {@code TIMESTAMP_TZ} gets an accessor that includes the time zone
+ * in {@code getString()}, while a plain TIMESTAMP column does not.
+ */
+ @Test public void testAccessorSelectionByTypeName() throws SQLException {
+ assertThat(getStringForColumn("TIMESTAMP_TZ"),
+ is("1970-01-01 00:00:00 UTC"));
+ assertThat(getStringForColumn("TIMESTAMP"),
+ is("1970-01-01 00:00:00"));
+ }
+
+ private static String getStringForColumn(String typeName) throws SQLException {
+ final ColumnMetaData metaData =
+ ColumnMetaData.dummy(
+ ColumnMetaData.scalar(Types.TIMESTAMP, typeName,
+ ColumnMetaData.Rep.LONG),
+ true);
+ final ListIteratorCursor cursor =
+ new ListIteratorCursor(
+ Collections.singletonList(
+ Collections.<Object>singletonList(0L)).iterator());
+ final List<Cursor.Accessor> accessors =
+ cursor.createAccessors(Collections.singletonList(metaData),
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT),
+ null);
+ assertThat(cursor.next(), is(true));
+ return accessors.get(0).getString();
+ }
+
+ /**
+ * Returns the value from the test instance to the accessor.
+ */
+ private class LocalGetter implements AbstractCursor.Getter {
+ @Override public Object getObject() {
+ return value;
+ }
+
+ @Override public boolean wasNull() {
+ return value == null;
+ }
+ }
+}