diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowArray.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowArray.java index f25523a45e70..81ef40d04d0e 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowArray.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowArray.java @@ -28,8 +28,7 @@ * An implementation of {@link BigQueryBaseArray} used to represent Array values from Arrow data. */ class BigQueryArrowArray extends BigQueryBaseArray { - private static final BigQueryTypeCoercer BIGQUERY_TYPE_COERCER = - BigQueryTypeCoercionUtility.INSTANCE; + private JsonStringArrayList values; public BigQueryArrowArray(Field schema, JsonStringArrayList values) { @@ -43,7 +42,7 @@ public BigQueryArrowArray( } @Override - public Object getArray() { + public Object getArray() throws SQLException { LOG.finestTrace("getArray"); ensureValid(); if (values == null) { @@ -53,7 +52,7 @@ public Object getArray() { } @Override - public Object getArray(long index, int count) { + public Object getArray(long index, int count) throws SQLException { LOG.finestTrace("getArray"); ensureValid(); if (values == null) { @@ -98,12 +97,12 @@ public void free() { } @Override - Object getCoercedValue(int index) { + Object getCoercedValue(int index) throws SQLException { LOG.finestTrace("getCoercedValue"); Object value = this.values.get(index); return this.arrayOfStruct ? new BigQueryArrowStruct( schema.getSubFields(), (JsonStringHashMap) value, this.LOG.getArrowStructLogger()) - : BIGQUERY_TYPE_COERCER.coerceTo(getTargetClass(), value, this.LOG); + : BigQueryTypeRegistry.convert(value, getTargetClass()); } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSet.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSet.java index 3123b6c09b40..86e82f1ce676 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSet.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSet.java @@ -31,10 +31,10 @@ import io.opentelemetry.context.Scope; import java.io.IOException; import java.math.BigDecimal; -import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -42,6 +42,7 @@ import java.util.concurrent.Future; import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.DateDayVector; import org.apache.arrow.vector.FieldVector; import org.apache.arrow.vector.VectorLoader; import org.apache.arrow.vector.VectorSchemaRoot; @@ -340,6 +341,9 @@ private Object getObjectInternal(int columnIndex) throws SQLException { FieldVector currentColumn = this.vectorSchemaRoot.getVector(columnIndex - 1); // get the current row value = currentColumn.getObject(this.currentBatchRowIndex); + if (value instanceof Integer && currentColumn instanceof DateDayVector) { + value = LocalDate.ofEpochDay(((Integer) value).longValue()); + } } setWasNull(value); return value; @@ -357,7 +361,7 @@ public Object getObject(int columnIndex) throws SQLException { } if (this.isNested && columnIndex == 1) { - return this.bigQueryTypeCoercer.coerceTo(Integer.class, value, this.LOG); + return BigQueryTypeRegistry.convert(value, Integer.class); } if (this.isNested && columnIndex == 2) { @@ -368,10 +372,11 @@ public Object getObject(int columnIndex) throws SQLException { (JsonStringHashMap) value, this.LOG.getArrowStructLogger()); } - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - arrayField.getType().getStandardType()); - return this.bigQueryTypeCoercer.coerceTo(targetClass, value, this.LOG); + if (value instanceof Integer + && arrayField.getType().getStandardType() == StandardSQLTypeName.DATE) { + value = LocalDate.ofEpochDay(((Integer) value).longValue()); + } + return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), null); } int fieldIndex = this.isNested ? 0 : columnIndex - 1; @@ -437,10 +442,7 @@ public Object getObject(int columnIndex) throws SQLException { // Strip trailing zeros to match JSON API and CLI output return ((BigDecimal) value).stripTrailingZeros(); } - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - fieldSchema.getType().getStandardType()); - return this.bigQueryTypeCoercer.coerceTo(targetClass, value, this.LOG); + return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null); } } @@ -460,24 +462,23 @@ private StandardSQLTypeName getElementTypeFromValue(Object element) { return StandardSQLTypeName.STRING; } - private String formatRangeElement(Object element, StandardSQLTypeName elementType) { + private String formatRangeElement(Object element, StandardSQLTypeName elementType) + throws SQLException { if (element == null) { return "UNBOUNDED"; } switch (elementType) { case DATE: // Arrow gives DATE as an Integer (days since epoch) - Date date = this.bigQueryTypeCoercer.coerceTo(Date.class, (Integer) element, this.LOG); - return date.toString(); + return LocalDate.ofEpochDay(((Integer) element).longValue()).toString(); case DATETIME: // Arrow gives DATETIME as a LocalDateTime - Timestamp dtTs = - this.bigQueryTypeCoercer.coerceTo(Timestamp.class, (LocalDateTime) element, this.LOG); - return this.bigQueryTypeCoercer.coerceTo(String.class, dtTs, this.LOG); + Timestamp dtTs = BigQueryTypeRegistry.convert((LocalDateTime) element, Timestamp.class); + return BigQueryTypeRegistry.convert(dtTs, String.class); case TIMESTAMP: // Arrow gives TIMESTAMP as a Long (microseconds since epoch) - Timestamp ts = this.bigQueryTypeCoercer.coerceTo(Timestamp.class, (Long) element, this.LOG); - return this.bigQueryTypeCoercer.coerceTo(String.class, ts, this.LOG); + Timestamp ts = BigQueryTypeRegistry.convert((Long) element, Timestamp.class); + return BigQueryTypeRegistry.convert(ts, String.class); default: // Fallback for any other unexpected type return element.toString(); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStruct.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStruct.java index e07406d996df..375c0619703a 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStruct.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStruct.java @@ -20,7 +20,10 @@ import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.FieldList; +import com.google.cloud.bigquery.StandardSQLTypeName; import java.lang.reflect.Array; +import java.sql.SQLException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import org.apache.arrow.vector.util.JsonStringArrayList; @@ -30,8 +33,6 @@ * An implementation of {@link BigQueryBaseStruct} used to represent Struct values from Arrow data. */ class BigQueryArrowStruct extends BigQueryBaseStruct { - private static final BigQueryTypeCoercer BIGQUERY_TYPE_COERCER = - BigQueryTypeCoercionUtility.INSTANCE; private final FieldList schema; @@ -54,7 +55,7 @@ FieldList getSchema() { } @Override - public Object[] getAttributes() { + public Object[] getAttributes() throws SQLException { LOG.finestTrace("getAttributes"); int size = this.schema.size(); Object[] attributes = (Object[]) Array.newInstance(Object.class, size); @@ -73,7 +74,7 @@ public Object[] getAttributes() { return attributes; } - private Object getValue(Field currentSchema, Object currentValue) { + private Object getValue(Field currentSchema, Object currentValue) throws SQLException { LOG.finestTrace("getValue"); if (isArray(currentSchema)) { return new BigQueryArrowArray( @@ -84,10 +85,12 @@ private Object getValue(Field currentSchema, Object currentValue) { (JsonStringHashMap) currentValue, this.LOG.getArrowStructLogger()); } else { - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - currentSchema.getType().getStandardType()); - return BIGQUERY_TYPE_COERCER.coerceTo(targetClass, currentValue, this.LOG); + if (currentValue instanceof Integer + && currentSchema.getType().getStandardType() == StandardSQLTypeName.DATE) { + currentValue = LocalDate.ofEpochDay(((Integer) currentValue).longValue()); + } + return BigQueryTypeRegistry.convert( + currentValue, currentSchema.getType().getStandardType(), null); } } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java index e001e35c6a21..d677f3182e8e 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java @@ -65,8 +65,7 @@ public final String getBaseTypeName() { public final int getBaseType() { LOG.finestTrace("getBaseType"); ensureValid(); - return BigQueryJdbcTypeMappings.standardSQLToJavaSqlTypesMapping.get( - schema.getType().getStandardType()); + return BigQueryTypeRegistry.toJdbcType(schema.getType().getStandardType()); } @Override @@ -91,7 +90,7 @@ public final ResultSet getResultSet(long index, int count, Map> throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } - protected Object getArrayInternal(int fromIndex, int toIndexExclusive) { + protected Object getArrayInternal(int fromIndex, int toIndexExclusive) throws SQLException { LOG.finestTrace("getArrayInternal"); Class targetClass = getTargetClass(); int size = toIndexExclusive - fromIndex; @@ -145,11 +144,10 @@ protected Class getTargetClass() { LOG.finestTrace("getTargetClass"); return this.arrayOfStruct ? Struct.class - : BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - this.schema.getType().getStandardType()); + : BigQueryTypeRegistry.toJavaClass(this.schema.getType().getStandardType()); } - abstract Object getCoercedValue(int index); + abstract Object getCoercedValue(int index) throws SQLException; static boolean isArray(Field currentSchema) { return currentSchema.getMode() == REPEATED; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java index 9216732b49b2..70000e749b24 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java @@ -27,8 +27,6 @@ import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.exception.BigQueryConversionException; -import com.google.cloud.bigquery.exception.BigQueryJdbcCoercionException; -import com.google.cloud.bigquery.exception.BigQueryJdbcCoercionNotFoundException; import com.google.cloud.bigquery.exception.BigQueryJdbcException; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; @@ -70,7 +68,7 @@ public abstract class BigQueryBaseResultSet extends BigQueryNoOpsResultSet private Job job; private SQLWarning warnings; private boolean warningsLoaded = false; - protected final BigQueryTypeCoercer bigQueryTypeCoercer = BigQueryTypeCoercionUtility.INSTANCE; + protected final SpanContext originalSpanContext; protected BigQueryBaseResultSet( @@ -297,7 +295,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { if (value == null) { return null; } - return this.bigQueryTypeCoercer.coerceTo(type, value, this.LOG); + return BigQueryTypeRegistry.convert(value, type); } catch (RuntimeException e) { throw createCoercionException(columnIndex, type, e); } @@ -323,8 +321,8 @@ public String getString(int columnIndex) throws SQLException { LOG.finestTrace("getString"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(String.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, String.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, String.class, e); } } @@ -342,8 +340,8 @@ public boolean getBoolean(int columnIndex) throws SQLException { try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Boolean.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, Boolean.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Boolean.class, e); } } @@ -353,8 +351,8 @@ public byte getByte(int columnIndex) throws SQLException { LOG.finestTrace("getByte"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Byte.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Byte.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Byte.class, e); } } @@ -364,8 +362,8 @@ public short getShort(int columnIndex) throws SQLException { LOG.finestTrace("getShort"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Short.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Short.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Short.class, e); } } @@ -375,8 +373,8 @@ public int getInt(int columnIndex) throws SQLException { LOG.finestTrace("getInt"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Integer.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Integer.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Integer.class, e); } } @@ -386,8 +384,8 @@ public long getLong(int columnIndex) throws SQLException { LOG.finestTrace("getLong"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Long.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Long.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Long.class, e); } } @@ -397,8 +395,8 @@ public float getFloat(int columnIndex) throws SQLException { LOG.finestTrace("getFloat"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Float.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Float.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Float.class, e); } } @@ -408,8 +406,8 @@ public double getDouble(int columnIndex) throws SQLException { LOG.finestTrace("getDouble"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(Double.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, Double.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Double.class, e); } } @@ -421,8 +419,8 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException LOG.finestTrace("getBigDecimal"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(BigDecimal.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, BigDecimal.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, BigDecimal.class, e); } } @@ -432,8 +430,8 @@ public byte[] getBytes(int columnIndex) throws SQLException { LOG.finestTrace("getBytes"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(byte[].class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, byte[].class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, byte[].class, e); } } @@ -443,8 +441,8 @@ public Date getDate(int columnIndex) throws SQLException { LOG.finestTrace("getDate"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(java.sql.Date.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, java.sql.Date.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, java.sql.Date.class, e); } } @@ -458,8 +456,8 @@ public Time getTime(int columnIndex) throws SQLException { } try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(java.sql.Time.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, java.sql.Time.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, java.sql.Time.class, e); } } @@ -473,8 +471,8 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { } try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(java.sql.Timestamp.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException e) { + return BigQueryTypeRegistry.convert(value, java.sql.Timestamp.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, java.sql.Timestamp.class, e); } } @@ -484,8 +482,8 @@ public BigDecimal getBigDecimal(int columnIndex) throws SQLException { LOG.finestTrace("getBigDecimal"); try { Object value = getObject(columnIndex); - return this.bigQueryTypeCoercer.coerceTo(BigDecimal.class, value, this.LOG); - } catch (BigQueryJdbcCoercionNotFoundException | BigQueryJdbcCoercionException e) { + return BigQueryTypeRegistry.convert(value, BigDecimal.class); + } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, BigDecimal.class, e); } } 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 280c34aa15d6..a85fc3cce440 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 @@ -25,13 +25,13 @@ import com.google.cloud.bigquery.FieldValue; import com.google.cloud.bigquery.Schema; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; /** An implementation of {@link BigQueryBaseArray} used to represent Array values from Json data. */ @InternalApi class BigQueryJsonArray extends BigQueryBaseArray { - private static final BigQueryTypeCoercer BIGQUERY_TYPE_COERCER = - BigQueryTypeCoercionUtility.INSTANCE; + private List values; BigQueryJsonArray(Field schema, FieldValue values) { @@ -44,7 +44,7 @@ class BigQueryJsonArray extends BigQueryBaseArray { } @Override - public Object getArray() { + public Object getArray() throws SQLException { ensureValid(); LOG.finestTrace("getArray"); if (this.values == null) { @@ -54,7 +54,7 @@ public Object getArray() { } @Override - public Object getArray(long index, int count) { + public Object getArray(long index, int count) throws SQLException { ensureValid(); LOG.finestTrace("getArray"); if (this.values == null) { @@ -98,11 +98,11 @@ public void free() { } @Override - Object getCoercedValue(int index) { + Object getCoercedValue(int index) throws SQLException { FieldValue fieldValue = this.values.get(index); return this.arrayOfStruct ? new BigQueryJsonStruct( this.schema.getSubFields(), fieldValue, this.LOG.getJsonStructLogger()) - : BIGQUERY_TYPE_COERCER.coerceTo(getTargetClass(), fieldValue, this.LOG); + : BigQueryTypeRegistry.convert(fieldValue, getTargetClass()); } } 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 0dbda843d1e1..e08b87751fbb 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 @@ -225,7 +225,7 @@ public Object getObject(int columnIndex) throws SQLException { } if (this.isNested && columnIndex == 1) { - return this.bigQueryTypeCoercer.coerceTo(Integer.class, value, this.LOG); + return BigQueryTypeRegistry.convert(value, Integer.class); } if (this.isNested && columnIndex == 2) { @@ -234,10 +234,7 @@ public Object getObject(int columnIndex) throws SQLException { return new BigQueryJsonStruct( arrayField.getSubFields(), value, this.LOG.getJsonStructLogger()); } - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - arrayField.getType().getStandardType()); - return this.bigQueryTypeCoercer.coerceTo(targetClass, value, this.LOG); + return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), null); } int extraIndex = this.isNested ? 2 : 1; @@ -248,10 +245,7 @@ public Object getObject(int columnIndex) throws SQLException { return new BigQueryJsonStruct( fieldSchema.getSubFields(), value, this.LOG.getJsonStructLogger()); } else { - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - fieldSchema.getType().getStandardType()); - return this.bigQueryTypeCoercer.coerceTo(targetClass, value, this.LOG); + return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null); } } 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 ed39edbecf17..c8129f41b88b 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 @@ -23,6 +23,7 @@ import com.google.cloud.bigquery.FieldList; import com.google.cloud.bigquery.FieldValue; import java.lang.reflect.Array; +import java.sql.SQLException; import java.util.List; /** @@ -30,8 +31,6 @@ */ @InternalApi class BigQueryJsonStruct extends BigQueryBaseStruct { - private static final BigQueryTypeCoercer BIGQUERY_TYPE_COERCER = - BigQueryTypeCoercionUtility.INSTANCE; private final FieldList schema; private final List values; @@ -52,7 +51,7 @@ FieldList getSchema() { } @Override - public Object[] getAttributes() { + public Object[] getAttributes() throws SQLException { LOG.finestTrace("getAttributes"); int size = schema.size(); Object[] attributes = (Object[]) Array.newInstance(Object.class, size); @@ -66,7 +65,7 @@ public Object[] getAttributes() { return attributes; } - private Object getValue(Field currentSchema, FieldValue currentValue) { + private Object getValue(Field currentSchema, FieldValue currentValue) throws SQLException { LOG.finestTrace("getValue"); if (isArray(currentSchema)) { return new BigQueryJsonArray(currentSchema, currentValue, this.LOG.getJsonArrayLogger()); @@ -74,10 +73,8 @@ private Object getValue(Field currentSchema, FieldValue currentValue) { return new BigQueryJsonStruct( currentSchema.getSubFields(), currentValue, this.LOG.getJsonStructLogger()); } else { - Class targetClass = - BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get( - currentSchema.getType().getStandardType()); - return BIGQUERY_TYPE_COERCER.coerceTo(targetClass, currentValue, this.LOG); + return BigQueryTypeRegistry.convert( + currentValue, currentSchema.getType().getStandardType(), null); } } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java index b26cf78bac0a..db7912ff4b6a 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java @@ -16,6 +16,7 @@ package com.google.cloud.bigquery.jdbc; +import java.math.BigDecimal; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; @@ -94,6 +95,21 @@ public static Time boxTime(String val, ZoneId zoneId) { * the Calendar timezone is explicitly ignored per JDBC 4.2 spec. */ public static Timestamp boxTimestamp(String val) { + // Check if the value is a numeric float string (e.g. "1680174859.8202269" from JSON API) + try { + if (val.indexOf('-') < 0 + || (val.startsWith("-") + && val.indexOf('-', 1) < 0)) { // Quick check to ensure it's not a date string + BigDecimal bd = new BigDecimal(val); + long secondsLong = bd.longValue(); + int nanos = bd.remainder(BigDecimal.ONE).multiply(new BigDecimal(1_000_000_000)).intValue(); + Timestamp ts = new Timestamp(secondsLong * 1000L); + ts.setNanos(nanos); + return ts; + } + } catch (NumberFormatException ignored) { + } + String iso = val; // Handle the " UTC" suffix format if (iso.endsWith(" UTC")) { @@ -104,12 +120,33 @@ public static Timestamp boxTimestamp(String val) { if (iso.length() > 10 && iso.charAt(10) == ' ') { iso = iso.substring(0, 10) + 'T' + iso.substring(11); } + // If it doesn't have a timezone designator, assume UTC 'Z' + if (!iso.endsWith("Z") && !iso.contains("+") && iso.lastIndexOf('-') <= 10) { + iso = iso + "Z"; + } try { return Timestamp.from(Instant.parse(iso)); } catch (java.time.format.DateTimeParseException e) { // Fallback for non-standard formats - return Timestamp.valueOf(val); + String fallback = val; + if (fallback.indexOf('T') > 0) { + fallback = fallback.replace('T', ' '); + } + return Timestamp.valueOf(fallback); } } + + /** + * Converts milliseconds of the day to a local epoch millis anchored to 1970-01-01 in the given + * timezone. + */ + public static long getLocalMillis(long millisOfDay, ZoneId zoneId) { + ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault(); + return LocalTime.ofNanoOfDay(millisOfDay * 1_000_000L) + .atDate(LocalDate.of(1970, 1, 1)) + .atZone(targetZone) + .toInstant() + .toEpochMilli(); + } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java index 289428734567..e3f993c7b4cf 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java @@ -16,6 +16,8 @@ package com.google.cloud.bigquery.jdbc; +import com.google.cloud.bigquery.FieldValue; +import com.google.cloud.bigquery.Range; import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.exception.BigQueryJdbcSqlFeatureNotSupportedException; @@ -27,16 +29,22 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.Period; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Arrays; +import java.util.Base64; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.arrow.vector.PeriodDuration; /** * A central, bidirectional engine for resolving and coercing types between JDBC, Java, and @@ -48,6 +56,11 @@ final class BigQueryTypeRegistry { private static final Map, TypeDescriptor> DESCRIPTORS_BY_CLASS; private static final Map> DESCRIPTORS_BY_JDBC_TYPE; + private static final DateTimeFormatter TIMESTAMP_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"); + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + static { DESCRIPTORS_BY_ORDINAL = new TypeDescriptor[StandardSQLTypeName.values().length]; DESCRIPTORS_BY_CLASS = new ConcurrentHashMap<>(); @@ -82,6 +95,9 @@ static TypeDescriptor createBoolDescriptor() { if (val instanceof Boolean) { return val; } + if (val instanceof Number) { + return ((Number) val).longValue() != 0; + } if (val instanceof String) { return Boolean.parseBoolean((String) val); } @@ -95,7 +111,21 @@ static TypeDescriptor createStringDescriptor() { String.class, StandardSQLTypeName.STRING, Arrays.asList(String.class), - (val, targetClass, zone) -> String.valueOf(val)); + (val, targetClass, zone) -> { + if (val == null) { + return null; + } + if (val instanceof byte[]) { + return Base64.getEncoder().encodeToString((byte[]) val); + } + if (val instanceof Timestamp) { + return TIMESTAMP_FORMATTER.format(((Timestamp) val).toLocalDateTime()); + } + if (val instanceof Time) { + return TIME_FORMATTER.format(((Time) val).toLocalTime()); + } + return String.valueOf(val); + }); } static TypeDescriptor createInt64Descriptor() { @@ -106,17 +136,39 @@ static TypeDescriptor createInt64Descriptor() { Arrays.asList(Long.class, Integer.class, Short.class, Byte.class), (val, targetClass, zone) -> { long longVal; - if (val instanceof Number) longVal = ((Number) val).longValue(); - else if (val instanceof String) longVal = Long.parseLong((String) val); - else throw new BigQueryJdbcException("Cannot convert to INT64: " + val); + if (val instanceof Number) { + if (val instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) val; + if (bd.compareTo(new BigDecimal(Long.MAX_VALUE)) > 0 + || bd.compareTo(new BigDecimal(Long.MIN_VALUE)) < 0) { + throw new BigQueryJdbcException("Value out of range for Long: " + bd); + } + } + longVal = ((Number) val).longValue(); + } else if (val instanceof String) { + longVal = Long.parseLong((String) val); + } else if (val instanceof Boolean) { + longVal = (Boolean) val ? 1L : 0L; + } else { + throw new BigQueryJdbcException("Cannot convert to INT64: " + val); + } if (targetClass == Integer.class) { + if (longVal > Integer.MAX_VALUE || longVal < Integer.MIN_VALUE) { + throw new BigQueryJdbcException("Value out of range for Integer: " + longVal); + } return (int) longVal; } if (targetClass == Short.class) { + if (longVal > Short.MAX_VALUE || longVal < Short.MIN_VALUE) { + throw new BigQueryJdbcException("Value out of range for Short: " + longVal); + } return (short) longVal; } if (targetClass == Byte.class) { + if (longVal > Byte.MAX_VALUE || longVal < Byte.MIN_VALUE) { + throw new BigQueryJdbcException("Value out of range for Byte: " + longVal); + } return (byte) longVal; } return longVal; @@ -133,6 +185,7 @@ static TypeDescriptor createFloat64Descriptor() { double doubleVal; if (val instanceof Number) doubleVal = ((Number) val).doubleValue(); else if (val instanceof String) doubleVal = Double.parseDouble((String) val); + else if (val instanceof Boolean) doubleVal = (Boolean) val ? 1.0 : 0.0; else throw new BigQueryJdbcException("Cannot convert to FLOAT64: " + val); if (targetClass == Float.class) { @@ -158,6 +211,9 @@ static TypeDescriptor createNumericDescriptor() { if (val instanceof String) { return new BigDecimal((String) val); } + if (val instanceof Boolean) { + return (Boolean) val ? BigDecimal.ONE : BigDecimal.ZERO; + } throw new BigQueryJdbcException("Cannot convert to NUMERIC: " + val); }); } @@ -169,19 +225,52 @@ static TypeDescriptor createDateDescriptor() { StandardSQLTypeName.DATE, Arrays.asList(Date.class, LocalDate.class), (val, targetClass, zone) -> { - // TODO(Phase 3): Add native JSR-310 fast-path to bypass boxing for LocalDate + // Modern fast-path: Bypass intermediate object creation for JSR-310 targets + if (targetClass == LocalDate.class) { + if (val instanceof LocalDate) { + return val; + } else if (val instanceof Date) { + return ((Date) val).toLocalDate(); + } else if (val instanceof Timestamp) { + return ((Timestamp) val).toInstant().atOffset(ZoneOffset.UTC).toLocalDate(); + } else if (val instanceof java.sql.Time) { + throw new BigQueryJdbcException("Cannot convert to DATE: " + val); + } else if (val instanceof java.util.Date) { + return new Date(((java.util.Date) val).getTime()).toLocalDate(); + } else if (val instanceof LocalDateTime) { + return ((LocalDateTime) val).toLocalDate(); + } else if (val instanceof Integer) { + return LocalDate.ofEpochDay(((Integer) val).longValue()); + } else if (val instanceof String) { + return LocalDate.parse((String) val); + } else { + throw new BigQueryJdbcException("Cannot convert to DATE: " + val); + } + } + + // Legacy path: Box values into java.sql.Date Date sqlDate; - if (val instanceof Date) sqlDate = (Date) val; - else if (val instanceof java.util.Date) + if (val instanceof Date) { + sqlDate = (Date) val; + } else if (val instanceof Timestamp) { + sqlDate = + Date.valueOf(((Timestamp) val).toInstant().atOffset(ZoneOffset.UTC).toLocalDate()); + } else if (val instanceof java.sql.Time) { + throw new BigQueryJdbcException("Cannot convert to DATE: " + val); + } else if (val instanceof java.util.Date) { sqlDate = new Date(((java.util.Date) val).getTime()); - else if (val instanceof LocalDate) sqlDate = Date.valueOf((LocalDate) val); - else if (val instanceof String) + } else if (val instanceof LocalDate) { + sqlDate = Date.valueOf((LocalDate) val); + } else if (val instanceof LocalDateTime) { + sqlDate = Date.valueOf(((LocalDateTime) val).toLocalDate()); + } else if (val instanceof Integer) { + sqlDate = Date.valueOf(LocalDate.ofEpochDay(((Integer) val).longValue())); + } else if (val instanceof String) { sqlDate = BigQueryTemporalUtility.boxDate((String) val, zone); - else throw new BigQueryJdbcException("Cannot convert to DATE: " + val); - - if (targetClass == LocalDate.class) { - return sqlDate.toLocalDate(); + } else { + throw new BigQueryJdbcException("Cannot convert to DATE: " + val); } + return sqlDate; }); } @@ -193,19 +282,35 @@ static TypeDescriptor createDatetimeDescriptor() { StandardSQLTypeName.DATETIME, Arrays.asList(Timestamp.class, LocalDateTime.class), (val, targetClass, zone) -> { - // TODO(Phase 3): Add native JSR-310 fast-path to bypass boxing for LocalDateTime + // Modern fast-path: Bypass intermediate object creation for JSR-310 targets + if (targetClass == LocalDateTime.class) { + if (val instanceof LocalDateTime) { + return val; + } else if (val instanceof Timestamp) { + return ((Timestamp) val).toLocalDateTime(); + } else if (val instanceof java.util.Date) { + return new Timestamp(((java.util.Date) val).getTime()).toLocalDateTime(); + } else if (val instanceof String) { + return LocalDateTime.parse(((String) val).replace(' ', 'T')); + } else { + throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); + } + } + + // Legacy path: Box values into java.sql.Timestamp Timestamp ts; - if (val instanceof Timestamp) ts = (Timestamp) val; - else if (val instanceof java.util.Date) + if (val instanceof Timestamp) { + ts = (Timestamp) val; + } else if (val instanceof java.util.Date) { ts = new Timestamp(((java.util.Date) val).getTime()); - else if (val instanceof LocalDateTime) ts = Timestamp.valueOf((LocalDateTime) val); - else if (val instanceof String) + } else if (val instanceof LocalDateTime) { + ts = Timestamp.valueOf((LocalDateTime) val); + } else if (val instanceof String) { ts = BigQueryTemporalUtility.boxDateTime((String) val, zone); - else throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); - - if (targetClass == LocalDateTime.class) { - return ts.toLocalDateTime(); + } else { + throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); } + return ts; }); } @@ -217,26 +322,66 @@ static TypeDescriptor createTimestampDescriptor() { StandardSQLTypeName.TIMESTAMP, Arrays.asList(Timestamp.class, OffsetDateTime.class, Instant.class, ZonedDateTime.class), (val, targetClass, zone) -> { - // TODO(Phase 3): Add native JSR-310 fast-path to bypass boxing for Instant, etc. + // Modern fast-path: Bypass intermediate object creation for JSR-310 targets + if (targetClass == Instant.class + || targetClass == OffsetDateTime.class + || targetClass == ZonedDateTime.class) { + Instant instant; + if (val instanceof Instant) { + instant = (Instant) val; + } else if (val instanceof OffsetDateTime) { + instant = ((OffsetDateTime) val).toInstant(); + } else if (val instanceof ZonedDateTime) { + instant = ((ZonedDateTime) val).toInstant(); + } else if (val instanceof Timestamp) { + instant = ((Timestamp) val).toInstant(); + } else if (val instanceof java.util.Date) { + instant = Instant.ofEpochMilli(((java.util.Date) val).getTime()); + } else if (val instanceof LocalDateTime) { + instant = ((LocalDateTime) val).toInstant(ZoneOffset.UTC); + } else if (val instanceof Long) { + instant = Instant.EPOCH.plus((Long) val, java.time.temporal.ChronoUnit.MICROS); + } else if (val instanceof String) { + instant = BigQueryTemporalUtility.boxTimestamp((String) val).toInstant(); + } else { + throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); + } + + if (targetClass == Instant.class) { + return instant; + } + if (targetClass == OffsetDateTime.class) { + return instant.atOffset(ZoneOffset.UTC); + } + if (targetClass == ZonedDateTime.class) { + return instant.atZone(ZoneOffset.UTC); + } + } + + // Legacy path: Box values into java.sql.Timestamp Timestamp ts; - if (val instanceof Timestamp) ts = (Timestamp) val; - else if (val instanceof java.util.Date) + if (val instanceof Timestamp) { + ts = (Timestamp) val; + } else if (val instanceof java.util.Date) { ts = new Timestamp(((java.util.Date) val).getTime()); - else if (val instanceof Instant) ts = Timestamp.from((Instant) val); - else if (val instanceof OffsetDateTime) + } else if (val instanceof Instant) { + ts = Timestamp.from((Instant) val); + } else if (val instanceof OffsetDateTime) { ts = Timestamp.from(((OffsetDateTime) val).toInstant()); - else if (val instanceof ZonedDateTime) + } else if (val instanceof ZonedDateTime) { ts = Timestamp.from(((ZonedDateTime) val).toInstant()); - else if (val instanceof String) ts = BigQueryTemporalUtility.boxTimestamp((String) val); - else throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); - - if (targetClass == Instant.class) { - return ts.toInstant(); + } else if (val instanceof LocalDateTime) { + ts = Timestamp.from(((LocalDateTime) val).toInstant(ZoneOffset.UTC)); + } else if (val instanceof Long) { + ts = + Timestamp.from( + Instant.EPOCH.plus((Long) val, java.time.temporal.ChronoUnit.MICROS)); + } else if (val instanceof String) { + ts = BigQueryTemporalUtility.boxTimestamp((String) val); + } else { + throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); } - if (targetClass == OffsetDateTime.class) - return ts.toInstant().atOffset(java.time.ZoneOffset.UTC); - if (targetClass == ZonedDateTime.class) - return ts.toInstant().atZone(java.time.ZoneOffset.UTC); + return ts; }); } @@ -249,16 +394,28 @@ static TypeDescriptor createTimeDescriptor() { Arrays.asList(Time.class, LocalTime.class), (val, targetClass, zone) -> { if (targetClass == LocalTime.class && val instanceof String) { - // Phase 3 Fast Path: Parse directly to LocalTime to preserve microsecond precision + // Fast Path: Parse directly to LocalTime to preserve microsecond precision return LocalTime.parse((String) val); } Time sqlTime; if (val instanceof Time) sqlTime = (Time) val; - else if (val instanceof java.util.Date) + else if (val instanceof Timestamp) { + sqlTime = + Time.valueOf(((Timestamp) val).toInstant().atOffset(ZoneOffset.UTC).toLocalTime()); + } else if (val instanceof java.sql.Date) { + throw new BigQueryJdbcException("Cannot convert to TIME: " + val); + } else if (val instanceof java.util.Date) sqlTime = new Time(((java.util.Date) val).getTime()); else if (val instanceof LocalTime) sqlTime = Time.valueOf((LocalTime) val); - else if (val instanceof String) + else if (val instanceof LocalDateTime) { + long millisOfDay = ((LocalDateTime) val).toLocalTime().toNanoOfDay() / 1_000_000; + sqlTime = new Time(BigQueryTemporalUtility.getLocalMillis(millisOfDay, zone)); + } else if (val instanceof Long) { + long millisOfDay = (Long) val / 1000; + // Align with civil time anchoring + sqlTime = new Time(BigQueryTemporalUtility.getLocalMillis(millisOfDay, zone)); + } else if (val instanceof String) sqlTime = BigQueryTemporalUtility.boxTime((String) val, zone); else throw new BigQueryJdbcException("Cannot convert to TIME: " + val); @@ -281,6 +438,8 @@ static TypeDescriptor createBytesDescriptor() { (val, targetClass, zone) -> { if (val instanceof byte[]) { return val; + } else if (val instanceof String) { + return Base64.getDecoder().decode((String) val); } throw new BigQueryJdbcException("Cannot convert to BYTES: " + val); }); @@ -358,7 +517,58 @@ static TypeDescriptor createIntervalDescriptor() { String.class, StandardSQLTypeName.INTERVAL, Arrays.asList(String.class), - (val, targetClass, zone) -> String.valueOf(val)); + (val, targetClass, zone) -> { + if (val == null) return null; + if (val instanceof PeriodDuration) { + PeriodDuration pd = (PeriodDuration) val; + Period period = pd.getPeriod().normalized(); + StringBuilder builder = new StringBuilder(); + builder + .append(period.getYears()) + .append("-") + .append(period.getMonths()) + .append(" ") + .append(period.getDays()) + .append(" "); + Duration duration = pd.getDuration(); + if (duration.isNegative()) { + builder.append("-"); + duration = duration.negated(); + } + long hours = duration.toHours(); + duration = duration.minusHours(hours); + long minutes = duration.toMinutes(); + duration = duration.minusMinutes(minutes); + long seconds = duration.getSeconds(); + duration = duration.minusSeconds(seconds); + long microseconds = duration.toNanos() / 1000; + builder + .append(hours) + .append(":") + .append(minutes) + .append(":") + .append(seconds) + .append("."); + + if (microseconds == 0) { + builder.append("0"); + } else { + // Left pad to 6 digits to preserve mathematical correctness + // e.g. 50 microseconds -> "000050" (so it prints .000050, not .50) + String microsStr = String.format("%06d", microseconds); + + // Strip trailing zeroes to cleanly format the fraction + // e.g. 1000 microseconds -> "001000" -> "001" (prints .001) + int lastNonZero = microsStr.length() - 1; + while (lastNonZero >= 0 && microsStr.charAt(lastNonZero) == '0') { + lastNonZero--; + } + builder.append(microsStr.substring(0, lastNonZero + 1)); + } + return builder.toString().replaceFirst("--", "-"); + } + return String.valueOf(val); + }); } static TypeDescriptor createRangeDescriptor() { @@ -367,7 +577,17 @@ static TypeDescriptor createRangeDescriptor() { String.class, StandardSQLTypeName.RANGE, Arrays.asList(String.class), - (val, targetClass, zone) -> String.valueOf(val)); + (val, targetClass, zone) -> { + if (val == null) return null; + if (val instanceof Range) { + Range range = (Range) val; + String start = + range.getStart().isNull() ? "UNBOUNDED" : range.getStart().getStringValue(); + String end = range.getEnd().isNull() ? "UNBOUNDED" : range.getEnd().getStringValue(); + return String.format("[%s, %s)", start, end); + } + return String.valueOf(val); + }); } private static void register(TypeDescriptor descriptor) { @@ -486,6 +706,14 @@ public static T convert(Object input, Class targetClass) throws BigQueryJ if (input == null) { return null; } + if (input instanceof FieldValue) { + FieldValue fv = (FieldValue) input; + if (fv.isNull()) return null; + input = fv.getValue(); + } + if (targetClass.isInstance(input)) { + return (T) input; + } TypeDescriptor descriptor = getDescriptorForClass(targetClass); if (descriptor == null) { throw new BigQueryJdbcException("Unsupported target class: " + targetClass.getName()); @@ -507,6 +735,13 @@ public static Object convert(Object input, StandardSQLTypeName bqType, ZoneId zo if (input == null) { return null; } + if (input instanceof FieldValue) { + FieldValue fv = (FieldValue) input; + if (fv.isNull()) { + return null; + } + input = fv.getValue(); + } int ordinal = bqType.ordinal(); if (ordinal >= DESCRIPTORS_BY_ORDINAL.length || DESCRIPTORS_BY_ORDINAL[ordinal] == null) { throw new BigQueryJdbcException("No type descriptor registered for BigQuery type: " + bqType); diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java index 77e3e08f1a70..5b58b80f2765 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java @@ -3280,7 +3280,7 @@ public void testMetadataAndResultSetMetadataTypeMappingConsistency(StandardSQLTy } ColumnTypeInfo metadataTypeInfo = dbMetadata.mapBigQueryTypeToJdbc(field); - Integer resultSetType = BigQueryJdbcTypeMappings.standardSQLToJavaSqlTypesMapping.get(type); + Integer resultSetType = BigQueryTypeRegistry.toJdbcType(type); assertNotNull(resultSetType, "ResultSet mapping should exist for " + type); assertEquals( 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 537e20b60fea..30a66a2bf5fd 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 @@ -127,10 +127,10 @@ public static Collection data() { TIMESTAMP, arraySchemaAndValue( TIMESTAMP, - "1680174859.8202269", - "1680261259.8202269", - "1680347659.8202269", - "1680434059.8202269"), + "1680174859.820227", + "1680261259.820227", + "1680347659.820227", + "1680434059.820227"), new Timestamp[] { Timestamp.valueOf(aTimeStamp), // 2023-03-30 16:44:19.82 Timestamp.valueOf(aTimeStamp.plusDays(1)), 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 ae074fa19e84..5c8deeab85e3 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 @@ -241,7 +241,7 @@ public void structOfStructs() throws SQLException { public void structWithNullValue() throws SQLException { assertThat(structWithNullValue.getAttributes()) .isEqualTo( - Arrays.asList(0L, false, 0.0, null, null, null, null, null, null, null, null, null) + Arrays.asList(null, null, null, null, null, null, null, null, null, null, null, null) .toArray()); } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java new file mode 100644 index 000000000000..060882c750a5 --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed 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 + * + * https://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 com.google.cloud.bigquery.jdbc; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.cloud.bigquery.StandardSQLTypeName; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.apache.arrow.vector.PeriodDuration; +import org.junit.jupiter.api.Test; + +public class BigQueryTypeRegistryTest { + + @Test + public void testIntervalFormatting() throws Exception { + Period p = Period.of(1, 2, 3); + Duration d = + Duration.ofHours(4).plusMinutes(5).plusSeconds(6).plusNanos(78000); // 78 microseconds + PeriodDuration pd = new PeriodDuration(p, d); + String result = + BigQueryTypeRegistry.convert(pd, StandardSQLTypeName.INTERVAL, String.class, null); + assertThat(result).isEqualTo("1-2 3 4:5:6.000078"); + } + + @Test + public void testIntervalFormattingZeroMicroseconds() throws Exception { + Period p = Period.of(0, 0, 0); + Duration d = Duration.ofHours(1).plusMinutes(0).plusSeconds(0); + PeriodDuration pd = new PeriodDuration(p, d); + String result = + BigQueryTypeRegistry.convert(pd, StandardSQLTypeName.INTERVAL, String.class, null); + assertThat(result).isEqualTo("0-0 0 1:0:0.0"); + } + + @Test + public void testIntervalFormattingNegativeDuration() throws Exception { + Period p = Period.of(0, 0, 0); + Duration d = Duration.ofHours(-1).minusMinutes(5).minusSeconds(6).minusNanos(78000); + PeriodDuration pd = new PeriodDuration(p, d); + String result = + BigQueryTypeRegistry.convert(pd, StandardSQLTypeName.INTERVAL, String.class, null); + assertThat(result).isEqualTo("0-0 0 -1:5:6.000078"); + } + + @Test + public void testDateFormatting() throws Exception { + LocalDate localDate = LocalDate.of(2026, 8, 24); + assertThat(BigQueryTypeRegistry.convert(localDate, StandardSQLTypeName.DATE, Date.class, null)) + .isEqualTo(Date.valueOf("2026-08-24")); + } + + @Test + public void testTimeFormatting() throws Exception { + LocalTime localTime = LocalTime.of(15, 30, 45, 123456000); // 123.456 ms + assertThat(BigQueryTypeRegistry.convert(localTime, StandardSQLTypeName.TIME, Time.class, null)) + .isEqualTo(Time.valueOf("15:30:45")); + } + + @Test + public void testDatetimeFormatting() throws Exception { + LocalDateTime localDateTime = LocalDateTime.of(2026, 8, 24, 15, 30, 45, 123456000); + assertThat( + BigQueryTypeRegistry.convert( + localDateTime, StandardSQLTypeName.DATETIME, Timestamp.class, null)) + .isEqualTo(Timestamp.valueOf("2026-08-24 15:30:45.123456")); + } + + @Test + public void testTimestampFormatting() throws Exception { + ZonedDateTime zonedDateTime = + ZonedDateTime.of(2026, 8, 24, 15, 30, 45, 123456000, ZoneId.of("UTC")); + assertThat( + BigQueryTypeRegistry.convert( + zonedDateTime, StandardSQLTypeName.TIMESTAMP, Timestamp.class, null)) + .isEqualTo(Timestamp.from(zonedDateTime.toInstant())); + } + + @Test + public void testCoercionException() throws Exception { + assertThrows( + BigQueryJdbcException.class, + () -> + BigQueryTypeRegistry.convert( + "bad_number", StandardSQLTypeName.INT64, Integer.class, null)); + } +}