diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArray.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArray.java index 029a36e73c5b..cbe237a996fa 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArray.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArray.java @@ -35,11 +35,15 @@ class BigQueryJsonArray extends BigQueryBaseArray { private List values; BigQueryJsonArray(Field schema, FieldValue values) { - this(schema, values, BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonArray.class)); + this(schema, values, false, BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonArray.class)); } - BigQueryJsonArray(Field schema, FieldValue values, BigQueryJdbcResultSetLogger log) { - super(schema, log); + BigQueryJsonArray( + Field schema, + FieldValue values, + boolean enableTimestampPicos, + BigQueryJdbcResultSetLogger log) { + super(schema, enableTimestampPicos, log); this.values = (values == null || values.isNull()) ? null : values.getRepeatedValue(); } @@ -74,7 +78,11 @@ public ResultSet getResultSet() { BigQueryFieldValueListWrapper bigQueryFieldValueListWrapper = getNestedFieldValueListWrapper(FieldList.of(singleElementSchema()), this.values); return BigQueryJsonResultSet.getNestedResultSet( - Schema.of(this.schema), bigQueryFieldValueListWrapper, 0, this.values.size()); + Schema.of(this.schema), + bigQueryFieldValueListWrapper, + 0, + this.values.size(), + this.enableTimestampPicos); } @Override @@ -88,7 +96,11 @@ public ResultSet getResultSet(long index, int count) { BigQueryFieldValueListWrapper bigQueryFieldValueListWrapper = getNestedFieldValueListWrapper(FieldList.of(singleElementSchema()), this.values); return BigQueryJsonResultSet.getNestedResultSet( - Schema.of(this.schema), bigQueryFieldValueListWrapper, range.x(), range.y()); + Schema.of(this.schema), + bigQueryFieldValueListWrapper, + range.x(), + range.y(), + this.enableTimestampPicos); } @Override @@ -99,10 +111,24 @@ public void free() { @Override Object getCoercedValue(int index) throws SQLException { + LOG.finestTrace("getCoercedValue"); FieldValue fieldValue = this.values.get(index); - return this.arrayOfStruct - ? new BigQueryJsonStruct( - this.schema.getSubFields(), fieldValue, this.LOG.getJsonStructLogger()) - : BigQueryTypeRegistry.convert(fieldValue, this.schema.getType().getStandardType(), null); + if (fieldValue == null || fieldValue.isNull()) { + return null; + } + if (this.arrayOfStruct) { + return new BigQueryJsonStruct( + this.schema.getSubFields(), + fieldValue, + this.enableTimestampPicos, + this.LOG.getJsonStructLogger()); + } + if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(this.schema)) { + return BigQueryTemporalUtility.formatTimestampValue(fieldValue.getStringValue(), true); + } + if (this.enableTimestampPicos && BigQueryJsonResultSet.isRangeTimestamp(this.schema)) { + return BigQueryJsonResultSet.formatRangeTimestamp(fieldValue); + } + return BigQueryTypeRegistry.convert(fieldValue, this.schema.getType().getStandardType(), null); } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSet.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSet.java index e08b87751fbb..7aac998b0cc8 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSet.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSet.java @@ -24,7 +24,9 @@ import com.google.cloud.bigquery.FieldValue; import com.google.cloud.bigquery.FieldValue.Attribute; import com.google.cloud.bigquery.Job; +import com.google.cloud.bigquery.Range; import com.google.cloud.bigquery.Schema; +import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; import java.sql.ResultSet; import java.sql.SQLException; @@ -45,6 +47,7 @@ class BigQueryJsonResultSet extends BigQueryBaseResultSet { private final int fromIndex; private final int toIndexExclusive; private final Future[] ownedTasks; + private final boolean enableTimestampPicos; private BigQueryJsonResultSet( Schema schema, @@ -57,7 +60,8 @@ private BigQueryJsonResultSet( int toIndexExclusive, Future[] ownedTasks, BigQuery bigQuery, - Job job) { + Job job, + boolean enableTimestampPicos) { super(bigQuery, statement, schema, isNested, job); this.totalRows = totalRows; this.buffer = buffer; @@ -66,6 +70,7 @@ private BigQueryJsonResultSet( this.toIndexExclusive = toIndexExclusive; this.nestedRowIndex = fromIndex - 1; this.ownedTasks = ownedTasks; + this.enableTimestampPicos = enableTimestampPicos; } /** @@ -95,7 +100,18 @@ static BigQueryJsonResultSet of( Job job) { return new BigQueryJsonResultSet( - schema, totalRows, buffer, statement, false, null, -1, -1, ownedTasks, bigQuery, job); + schema, + totalRows, + buffer, + statement, + false, + null, + -1, + -1, + ownedTasks, + bigQuery, + job, + statement != null && statement.isEnableTimestampPicos()); } static BigQueryJsonResultSet of( @@ -106,7 +122,18 @@ static BigQueryJsonResultSet of( Future[] ownedTasks) { return new BigQueryJsonResultSet( - schema, totalRows, buffer, statement, false, null, -1, -1, ownedTasks, null, null); + schema, + totalRows, + buffer, + statement, + false, + null, + -1, + -1, + ownedTasks, + null, + null, + statement != null && statement.isEnableTimestampPicos()); } static BigQueryJsonResultSet of( @@ -133,6 +160,7 @@ static BigQueryJsonResultSet of( fromIndex = 0; ownedTasks = new Future[0]; toIndexExclusive = 0; + this.enableTimestampPicos = false; } // @@ -145,10 +173,15 @@ static BigQueryJsonResultSet of( * @param cursor Points to the current record * @param fromIndex starting index under consideration * @param toIndexExclusive last index under consideration + * @param enableTimestampPicos whether picosecond timestamp precision is enabled * @return The BigQueryJsonResultSet */ static BigQueryJsonResultSet getNestedResultSet( - Schema schema, BigQueryFieldValueListWrapper cursor, int fromIndex, int toIndexExclusive) { + Schema schema, + BigQueryFieldValueListWrapper cursor, + int fromIndex, + int toIndexExclusive, + boolean enableTimestampPicos) { return new BigQueryJsonResultSet( schema, -1, @@ -160,7 +193,8 @@ static BigQueryJsonResultSet getNestedResultSet( toIndexExclusive, null, null, - null); + null, + enableTimestampPicos); } /* Advances the result set to the next row, returning false if no such row exists. Potentially blocking operation */ @@ -232,21 +266,62 @@ public Object getObject(int columnIndex) throws SQLException { Field arrayField = this.schema.getFields().get(0); if (isStruct(arrayField)) { return new BigQueryJsonStruct( - arrayField.getSubFields(), value, this.LOG.getJsonStructLogger()); + arrayField.getSubFields(), + value, + this.enableTimestampPicos, + this.LOG.getJsonStructLogger()); + } + if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(arrayField)) { + return BigQueryTemporalUtility.formatTimestampValue(value.getStringValue(), true); + } + if (this.enableTimestampPicos && isRangeTimestamp(arrayField)) { + return formatRangeTimestamp(value); } return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), null); } - int extraIndex = this.isNested ? 2 : 1; - Field fieldSchema = this.schemaFieldList.get(columnIndex - extraIndex); + Field fieldSchema = this.schemaFieldList.get(columnIndex - 1); if (isArray(fieldSchema)) { - return new BigQueryJsonArray(fieldSchema, value, this.LOG.getJsonArrayLogger()); - } else if (isStruct(fieldSchema)) { + return new BigQueryJsonArray( + fieldSchema, value, this.enableTimestampPicos, this.LOG.getJsonArrayLogger()); + } + if (isStruct(fieldSchema)) { return new BigQueryJsonStruct( - fieldSchema.getSubFields(), value, this.LOG.getJsonStructLogger()); - } else { - return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null); + fieldSchema.getSubFields(), + value, + this.enableTimestampPicos, + this.LOG.getJsonStructLogger()); } + if (this.enableTimestampPicos && isRangeTimestamp(fieldSchema)) { + return formatRangeTimestamp(value); + } + if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(fieldSchema)) { + return BigQueryTemporalUtility.formatTimestampValue(value.getStringValue(), true); + } + return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null); + } + + static String formatRangeTimestamp(FieldValue value) throws SQLException { + Range range = value.getRangeValue(); + String start = + range.getStart().isNull() + ? "UNBOUNDED" + : BigQueryTemporalUtility.formatTimestampValue(range.getStart().getStringValue(), true); + String end = + range.getEnd().isNull() + ? "UNBOUNDED" + : BigQueryTemporalUtility.formatTimestampValue(range.getEnd().getStringValue(), true); + return String.format("[%s, %s)", start, end); + } + + static boolean isRangeTimestamp(Field field) { + return field != null + && field.getRangeElementType() != null + && field.getRangeElementType().getType() != null + && field + .getRangeElementType() + .getType() + .equalsIgnoreCase(StandardSQLTypeName.TIMESTAMP.name()); } /** diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStruct.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStruct.java index c8129f41b88b..041d45650222 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStruct.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStruct.java @@ -36,11 +36,15 @@ class BigQueryJsonStruct extends BigQueryBaseStruct { private final List values; public BigQueryJsonStruct(FieldList schema, FieldValue values) { - this(schema, values, BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonStruct.class)); + this(schema, values, false, BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonStruct.class)); } - public BigQueryJsonStruct(FieldList schema, FieldValue values, BigQueryJdbcResultSetLogger log) { - super(log); + public BigQueryJsonStruct( + FieldList schema, + FieldValue values, + boolean enableTimestampPicos, + BigQueryJdbcResultSetLogger log) { + super(enableTimestampPicos, log); this.schema = schema; this.values = (values == null || values.isNull()) ? null : values.getRecordValue(); } @@ -67,14 +71,27 @@ public Object[] getAttributes() throws SQLException { private Object getValue(Field currentSchema, FieldValue currentValue) throws SQLException { LOG.finestTrace("getValue"); + if (currentValue == null || currentValue.isNull()) { + return null; + } if (isArray(currentSchema)) { - return new BigQueryJsonArray(currentSchema, currentValue, this.LOG.getJsonArrayLogger()); - } else if (isStruct(currentSchema)) { + return new BigQueryJsonArray( + currentSchema, currentValue, this.enableTimestampPicos, this.LOG.getJsonArrayLogger()); + } + if (isStruct(currentSchema)) { return new BigQueryJsonStruct( - currentSchema.getSubFields(), currentValue, this.LOG.getJsonStructLogger()); - } else { - return BigQueryTypeRegistry.convert( - currentValue, currentSchema.getType().getStandardType(), null); + currentSchema.getSubFields(), + currentValue, + this.enableTimestampPicos, + this.LOG.getJsonStructLogger()); + } + if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(currentSchema)) { + return BigQueryTemporalUtility.formatTimestampValue(currentValue.getStringValue(), true); + } + if (this.enableTimestampPicos && BigQueryJsonResultSet.isRangeTimestamp(currentSchema)) { + return BigQueryJsonResultSet.formatRangeTimestamp(currentValue); } + return BigQueryTypeRegistry.convert( + currentValue, currentSchema.getType().getStandardType(), null); } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java index 7ce5e6fba529..38a5df969406 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java @@ -486,7 +486,7 @@ public void testJsonArrayConnectionIdPropagation() { Attribute.REPEATED, FieldValueList.of( Collections.singletonList(FieldValue.of(Attribute.PRIMITIVE, "123")))); - BigQueryJsonArray array = new BigQueryJsonArray(arraySchema, arrayValue, logger); + BigQueryJsonArray array = new BigQueryJsonArray(arraySchema, arrayValue, false, logger); assertConnectionIdPropagated( logger, connectionId, "Log from JSON Array", () -> array.LOG.fine("Log from JSON Array")); @@ -504,7 +504,8 @@ public void testJsonStructConnectionIdPropagation() { Attribute.RECORD, FieldValueList.of( Collections.singletonList(FieldValue.of(Attribute.PRIMITIVE, "456")))); - BigQueryJsonStruct struct = new BigQueryJsonStruct(structSchema, structValue, structLogger); + BigQueryJsonStruct struct = + new BigQueryJsonStruct(structSchema, structValue, false, structLogger); assertConnectionIdPropagated( structLogger, @@ -531,7 +532,8 @@ public void testNestedStructInJsonArrayConnectionIdPropagation() throws Exceptio Collections.singletonList(FieldValue.of(Attribute.PRIMITIVE, "789")))); FieldValue listVal = FieldValue.of(Attribute.REPEATED, FieldValueList.of(Collections.singletonList(recordVal))); - BigQueryJsonArray arrayWithNested = new BigQueryJsonArray(arrayNestedSchema, listVal, logger); + BigQueryJsonArray arrayWithNested = + new BigQueryJsonArray(arrayNestedSchema, listVal, false, logger); Object[] result = (Object[]) arrayWithNested.getArray(); BigQueryJsonStruct nestedStruct = (BigQueryJsonStruct) result[0]; @@ -565,7 +567,7 @@ public void testNestedArrayInJsonStructConnectionIdPropagation() throws Exceptio FieldValue.of(Attribute.RECORD, FieldValueList.of(Collections.singletonList(arrayVal))); BigQueryJsonStruct structWithNestedArray = - new BigQueryJsonStruct(nestedSchema, rootVal, structLogger); + new BigQueryJsonStruct(nestedSchema, rootVal, false, structLogger); Object[] attributes = structWithNestedArray.getAttributes(); BigQueryJsonArray nestedArray = (BigQueryJsonArray) attributes[0]; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArrayOfPrimitivesTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArrayOfPrimitivesTest.java index 30a66a2bf5fd..f2127a552077 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArrayOfPrimitivesTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonArrayOfPrimitivesTest.java @@ -41,6 +41,7 @@ import com.google.cloud.Tuple; import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.FieldValue; +import com.google.cloud.bigquery.FieldValueList; import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.jdbc.rules.TimeZoneRule; import com.google.common.io.BaseEncoding; @@ -59,8 +60,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; @@ -383,4 +386,76 @@ private void ensureArrayIsInvalid(Executable block) { Exception exception = assertThrows(IllegalStateException.class, block); assertThat(exception.getMessage()).isEqualTo(INVALID_ARRAY); } + + @Test + public void testJsonArrayTimestampPicosEnabled() throws SQLException { + Field field = + Field.newBuilder("picosArray", StandardSQLTypeName.TIMESTAMP) + .setMode(Field.Mode.REPEATED) + .setTimestampPrecision(12L) + .build(); + FieldValue val1 = FieldValue.of(FieldValue.Attribute.PRIMITIVE, "1680174859.123456789123"); + FieldValue val2 = + FieldValue.of(FieldValue.Attribute.PRIMITIVE, "2026-04-08T11:00:00.987654321012Z"); + FieldValue arrayValue = + FieldValue.of(FieldValue.Attribute.REPEATED, FieldValueList.of(Arrays.asList(val1, val2))); + + BigQueryJsonArray array = + new BigQueryJsonArray( + field, + arrayValue, + true, + BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonArray.class)); + + assertThat(array.getBaseTypeName()).isEqualTo("TIMESTAMP"); + assertThat(array.getBaseType()).isEqualTo(Types.TIMESTAMP); + + Object result = array.getArray(); + assertThat(result).isInstanceOf(String[].class); + assertThat((String[]) result) + .asList() + .containsExactly("2023-03-30 11:14:19.123456789123", "2026-04-08 11:00:00.987654321012") + .inOrder(); + + ResultSet rs = array.getResultSet(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getString(2)).isEqualTo("2023-03-30 11:14:19.123456789123"); + assertThat(rs.getObject(2)).isEqualTo("2023-03-30 11:14:19.123456789123"); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(2); + assertThat(rs.getString(2)).isEqualTo("2026-04-08 11:00:00.987654321012"); + assertThat(rs.getObject(2)).isEqualTo("2026-04-08 11:00:00.987654321012"); + } + + @Test + public void testJsonArrayTimestampPicosDisabled() throws SQLException { + Field field = + Field.newBuilder("picosArray", StandardSQLTypeName.TIMESTAMP) + .setMode(Field.Mode.REPEATED) + .setTimestampPrecision(12L) + .build(); + FieldValue val1 = FieldValue.of(FieldValue.Attribute.PRIMITIVE, "1680174859.123456789123"); + FieldValue arrayValue = + FieldValue.of( + FieldValue.Attribute.REPEATED, FieldValueList.of(Collections.singletonList(val1))); + + BigQueryJsonArray array = + new BigQueryJsonArray( + field, + arrayValue, + false, + BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonArray.class)); + + Object result = array.getArray(); + assertThat(result).isInstanceOf(Timestamp[].class); + Timestamp expectedTs = Timestamp.valueOf("2023-03-30 11:14:19.123456789"); + assertThat((Timestamp[]) result).asList().containsExactly(expectedTs); + + ResultSet rs = array.getResultSet(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(2)).isEqualTo("2023-03-30 11:14:19.123456"); + assertThat(rs.getObject(2)).isEqualTo(expectedTs); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java index 06af37010d25..f44e42595a1b 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java @@ -19,14 +19,17 @@ import static com.google.common.truth.Truth.assertThat; import static java.time.Month.MARCH; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import com.google.cloud.bigquery.Field; +import com.google.cloud.bigquery.FieldElementType; import com.google.cloud.bigquery.FieldList; import com.google.cloud.bigquery.FieldValue; import com.google.cloud.bigquery.FieldValue.Attribute; import com.google.cloud.bigquery.FieldValueList; import com.google.cloud.bigquery.LegacySQLTypeName; +import com.google.cloud.bigquery.Range; import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.jdbc.rules.TimeZoneRule; @@ -195,7 +198,8 @@ public void setUp() { Schema.of(fieldEight), bigQueryFieldValueListWrapperNested, 0, - fieldEightValue.getRepeatedValue().size()); + fieldEightValue.getRepeatedValue().size(), + false); } private boolean resetResultSet() @@ -513,4 +517,187 @@ private int resultSetRowCount(BigQueryJsonResultSet resultSet) throws SQLExcepti } return rowCount; } + + private BigQueryJsonResultSet createJsonResultSet( + FieldList schemaFields, FieldValueList values, boolean enableTimestampPicos) { + Schema schema = Schema.of(schemaFields); + boolean[] isComplex = BigQueryFieldValueListWrapper.createComplexColumnFlags(schemaFields); + BlockingQueue picosBuffer = new LinkedBlockingDeque<>(2); + picosBuffer.add(BigQueryFieldValueListWrapper.of(schemaFields, values, isComplex)); + picosBuffer.add(BigQueryFieldValueListWrapper.ofEndOfStream(null)); + BigQueryStatement stmt = mock(BigQueryStatement.class); + doReturn(enableTimestampPicos).when(stmt).isEnableTimestampPicos(); + return BigQueryJsonResultSet.of( + schema, 1L, picosBuffer, stmt, new Future[] {mock(Future.class)}); + } + + @Test + public void testTimestampPicosEnabledWithDecimalEpoch() throws SQLException { + Field picosField = + Field.newBuilder("picosTs", StandardSQLTypeName.TIMESTAMP) + .setTimestampPrecision(12L) + .build(); + FieldList fields = FieldList.of(picosField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of(FieldValue.of(Attribute.PRIMITIVE, "1680174859.123456789123")), + fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("picosTs")).isEqualTo("2023-03-30 11:14:19.123456789123"); + assertThat(rs.getString(1)).isEqualTo("2023-03-30 11:14:19.123456789123"); + assertThat(rs.getObject("picosTs")).isEqualTo("2023-03-30 11:14:19.123456789123"); + assertThat(rs.getObject(1)).isEqualTo("2023-03-30 11:14:19.123456789123"); + assertThat(rs.getObject("picosTs", String.class)).isEqualTo("2023-03-30 11:14:19.123456789123"); + + Timestamp expectedTs = Timestamp.valueOf("2023-03-30 11:14:19.123456789"); + assertThat(rs.getTimestamp("picosTs")).isEqualTo(expectedTs); + assertThat(rs.getObject("picosTs", Timestamp.class)).isEqualTo(expectedTs); + } + + @Test + public void testTimestampPicosEnabledWithIsoString() throws SQLException { + Field picosField = + Field.newBuilder("picosTs", StandardSQLTypeName.TIMESTAMP) + .setTimestampPrecision(12L) + .build(); + FieldList fields = FieldList.of(picosField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of( + FieldValue.of(Attribute.PRIMITIVE, "2026-04-08T10:00:00.123456789123Z")), + fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("picosTs")).isEqualTo("2026-04-08 10:00:00.123456789123"); + assertThat(rs.getObject("picosTs")).isEqualTo("2026-04-08 10:00:00.123456789123"); + assertThat(rs.getObject(1)).isEqualTo("2026-04-08 10:00:00.123456789123"); + } + + @Test + public void testTimestampPicosEnabledWithScientificNotation() throws SQLException { + Field picosField = + Field.newBuilder("picosTs", StandardSQLTypeName.TIMESTAMP) + .setTimestampPrecision(12L) + .build(); + FieldList fields = FieldList.of(picosField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of(FieldValue.of(Attribute.PRIMITIVE, "1.6905474E9")), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("picosTs")).isEqualTo("2023-07-28 12:30:00.000000000000"); + } + + @Test + public void testTimestampPicosEnabledWithNegativeEpoch() throws SQLException { + Field picosField = + Field.newBuilder("picosTs", StandardSQLTypeName.TIMESTAMP) + .setTimestampPrecision(12L) + .build(); + FieldList fields = FieldList.of(picosField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of(FieldValue.of(Attribute.PRIMITIVE, "-0.123456789123")), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("picosTs")).isEqualTo("1969-12-31 23:59:59.876543210877"); + } + + @Test + public void testTimestampPicosDisabledWith12DigitField() throws SQLException { + Field picosField = + Field.newBuilder("picosTs", StandardSQLTypeName.TIMESTAMP) + .setTimestampPrecision(12L) + .build(); + FieldList fields = FieldList.of(picosField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of(FieldValue.of(Attribute.PRIMITIVE, "1680174859.123456789123")), + fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, false); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("picosTs")).isEqualTo("2023-03-30 11:14:19.123456"); + Timestamp expectedTs = Timestamp.valueOf("2023-03-30 11:14:19.123456789"); + assertThat(rs.getObject("picosTs")).isEqualTo(expectedTs); + assertThat(rs.getTimestamp("picosTs")).isEqualTo(expectedTs); + } + + @Test + public void testStandardTimestampWithPicosEnabled() throws SQLException { + Field stdField = Field.of("stdTs", StandardSQLTypeName.TIMESTAMP); + FieldList fields = FieldList.of(stdField); + FieldValueList row = + FieldValueList.of( + ImmutableList.of(FieldValue.of(Attribute.PRIMITIVE, "1680174859.820000")), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("stdTs")).isEqualTo("2023-03-30 11:14:19.820000"); + Timestamp expectedTs = Timestamp.valueOf("2023-03-30 11:14:19.820"); + assertThat(rs.getObject("stdTs")).isEqualTo(expectedTs); + } + + @Test + public void testRangeTimestampPicosEnabled() throws SQLException { + Field rangeField = + Field.newBuilder("rangeTs", StandardSQLTypeName.RANGE) + .setRangeElementType(FieldElementType.newBuilder().setType("TIMESTAMP").build()) + .build(); + FieldList fields = FieldList.of(rangeField); + Range range = Range.of("[1680174859.123456789123, 1680174860.123456789123)"); + FieldValueList row = + FieldValueList.of(ImmutableList.of(FieldValue.of(Attribute.RANGE, range)), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("rangeTs")) + .isEqualTo("[2023-03-30 11:14:19.123456789123, 2023-03-30 11:14:20.123456789123)"); + assertThat(rs.getObject("rangeTs")) + .isEqualTo("[2023-03-30 11:14:19.123456789123, 2023-03-30 11:14:20.123456789123)"); + } + + @Test + public void testRangeTimestampPicosDisabled() throws SQLException { + Field rangeField = + Field.newBuilder("rangeTs", StandardSQLTypeName.RANGE) + .setRangeElementType(FieldElementType.newBuilder().setType("TIMESTAMP").build()) + .build(); + FieldList fields = FieldList.of(rangeField); + Range range = Range.of("[1680174859.123456789123, 1680174860.123456789123)"); + FieldValueList row = + FieldValueList.of(ImmutableList.of(FieldValue.of(Attribute.RANGE, range)), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, false); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("rangeTs")) + .isEqualTo("[1680174859.123456789123, 1680174860.123456789123)"); + } + + @Test + public void testRangeTimestampUnbounded() throws SQLException { + Field rangeField = + Field.newBuilder("rangeTs", StandardSQLTypeName.RANGE) + .setRangeElementType(FieldElementType.newBuilder().setType("TIMESTAMP").build()) + .build(); + FieldList fields = FieldList.of(rangeField); + Range range = Range.of("[UNBOUNDED, 1680174860.123456789123)"); + FieldValueList row = + FieldValueList.of(ImmutableList.of(FieldValue.of(Attribute.RANGE, range)), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("rangeTs")).isEqualTo("[UNBOUNDED, 2023-03-30 11:14:20.123456789123)"); + } + + @Test + public void testRangeDate() throws SQLException { + Field rangeField = + Field.newBuilder("rangeDate", StandardSQLTypeName.RANGE) + .setRangeElementType(FieldElementType.newBuilder().setType("DATE").build()) + .build(); + FieldList fields = FieldList.of(rangeField); + Range range = Range.of("[2023-01-01, 2023-01-31)"); + FieldValueList row = + FieldValueList.of(ImmutableList.of(FieldValue.of(Attribute.RANGE, range)), fields); + BigQueryJsonResultSet rs = createJsonResultSet(fields, row, true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("rangeDate")).isEqualTo("[2023-01-01, 2023-01-31)"); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java index 5c8deeab85e3..6620cab2ca5c 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java @@ -258,4 +258,41 @@ public void getAttributesWithCustomTypeMappingsIsNotSupported() { () -> structWithPrimitiveValues.getAttributes(emptyMap())); assertThat(exception.getMessage()).isEqualTo(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } + + @Test + public void testJsonStructTimestampPicosEnabled() throws SQLException { + Field picosField = Field.newBuilder("picosTs", TIMESTAMP).setTimestampPrecision(12L).build(); + FieldList schema = FieldList.of(picosField); + FieldValue val = FieldValue.of(PRIMITIVE, "1680174859.123456789123"); + FieldValue structValue = FieldValue.of(RECORD, FieldValueList.of(asList(val))); + + BigQueryJsonStruct struct = + new BigQueryJsonStruct( + schema, + structValue, + true, + BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonStruct.class)); + + Object[] attributes = struct.getAttributes(); + assertThat(attributes).isEqualTo(new Object[] {"2023-03-30 11:14:19.123456789123"}); + } + + @Test + public void testJsonStructTimestampPicosDisabled() throws SQLException { + Field picosField = Field.newBuilder("picosTs", TIMESTAMP).setTimestampPrecision(12L).build(); + FieldList schema = FieldList.of(picosField); + FieldValue val = FieldValue.of(PRIMITIVE, "1680174859.123456789123"); + FieldValue structValue = FieldValue.of(RECORD, FieldValueList.of(asList(val))); + + BigQueryJsonStruct struct = + new BigQueryJsonStruct( + schema, + structValue, + false, + BigQueryJdbcResultSetLogger.getLogger(BigQueryJsonStruct.class)); + + Object[] attributes = struct.getAttributes(); + Timestamp expectedTs = Timestamp.valueOf("2023-03-30 11:14:19.123456789"); + assertThat(attributes).isEqualTo(new Object[] {expectedTs}); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryResultSetMetadataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryResultSetMetadataTest.java index 8261a14dc981..f124901ca80c 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryResultSetMetadataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryResultSetMetadataTest.java @@ -100,7 +100,7 @@ public void setUp() throws SQLException { BigQueryFieldValueListWrapper.getNestedFieldValueListWrapper(nestedFieldList, null); BigQueryJsonResultSet bigQueryJsonResultSetNested = BigQueryJsonResultSet.getNestedResultSet( - Schema.of(nestedFieldList), bigQueryFieldValueListWrapperNested, -1, -1); + Schema.of(nestedFieldList), bigQueryFieldValueListWrapperNested, -1, -1, false); resultSetMetaDataNested = bigQueryJsonResultSetNested.getMetaData(); }