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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading