From 436c18e91364879a74941e0f65bd8a416c610c43 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 12:43:58 -0400 Subject: [PATCH 1/7] feat(bigquery-jdbc): implement TypeRegistry and TypeDescriptor --- .../bigquery/jdbc/BigQueryTypeRegistry.java | 319 ++++++++++++++++++ .../cloud/bigquery/jdbc/TypeDescriptor.java | 71 ++++ 2 files changed, 390 insertions(+) create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java 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 new file mode 100644 index 000000000000..863917154895 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java @@ -0,0 +1,319 @@ +/* + * 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 com.google.cloud.bigquery.StandardSQLTypeName; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Date; +import java.sql.Struct; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * A central, bidirectional engine for resolving and coercing types between JDBC, Java, and + * BigQuery. + */ +public final class BigQueryTypeRegistry { + + private static final TypeDescriptor[] DESCRIPTORS_BY_ORDINAL; + private static final Map, TypeDescriptor> DESCRIPTORS_BY_CLASS; + private static final Map> DESCRIPTORS_BY_JDBC_TYPE; + + static { + int maxOrdinal = 0; + for (StandardSQLTypeName type : StandardSQLTypeName.values()) { + if (type.ordinal() > maxOrdinal) { + maxOrdinal = type.ordinal(); + } + } + DESCRIPTORS_BY_ORDINAL = new TypeDescriptor[maxOrdinal + 1]; + DESCRIPTORS_BY_CLASS = new IdentityHashMap<>(); + DESCRIPTORS_BY_JDBC_TYPE = new HashMap<>(); + + // BOOL + register( + new TypeDescriptor<>( + Types.BOOLEAN, + Boolean.class, + StandardSQLTypeName.BOOL, + Arrays.asList(Boolean.class), + (val, zone) -> { + if (val instanceof Boolean) return (Boolean) val; + if (val instanceof String) return Boolean.parseBoolean((String) val); + throw new IllegalArgumentException("Cannot convert to BOOL: " + val); + })); + + // STRING + register( + new TypeDescriptor<>( + Types.NVARCHAR, + String.class, + StandardSQLTypeName.STRING, + Arrays.asList(String.class), + (val, zone) -> String.valueOf(val))); + + // INT64 + register( + new TypeDescriptor<>( + Types.BIGINT, + Long.class, + StandardSQLTypeName.INT64, + Arrays.asList(Long.class, Integer.class, Short.class, Byte.class), + (val, zone) -> { + if (val instanceof Number) return ((Number) val).longValue(); + if (val instanceof String) return Long.parseLong((String) val); + throw new IllegalArgumentException("Cannot convert to INT64: " + val); + })); + + // FLOAT64 + register( + new TypeDescriptor<>( + Types.DOUBLE, + Double.class, + StandardSQLTypeName.FLOAT64, + Arrays.asList(Double.class, Float.class), + (val, zone) -> { + if (val instanceof Number) return ((Number) val).doubleValue(); + if (val instanceof String) return Double.parseDouble((String) val); + throw new IllegalArgumentException("Cannot convert to FLOAT64: " + val); + })); + + // NUMERIC + register( + new TypeDescriptor<>( + Types.NUMERIC, + BigDecimal.class, + StandardSQLTypeName.NUMERIC, + Arrays.asList(BigDecimal.class), + (val, zone) -> { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + if (val instanceof String) return new BigDecimal((String) val); + throw new IllegalArgumentException("Cannot convert to NUMERIC: " + val); + })); + + // DATE + register( + new TypeDescriptor<>( + Types.DATE, + Date.class, + StandardSQLTypeName.DATE, + Arrays.asList(Date.class, LocalDate.class), + (val, zone) -> { + if (val instanceof Date) return (Date) val; + if (val instanceof LocalDate) return Date.valueOf((LocalDate) val); + throw new IllegalArgumentException("Cannot convert to DATE: " + val); + })); + + // DATETIME + register( + new TypeDescriptor<>( + Types.TIMESTAMP, + Timestamp.class, + StandardSQLTypeName.DATETIME, + Arrays.asList(LocalDateTime.class), + (val, zone) -> { + if (val instanceof LocalDateTime) return Timestamp.valueOf((LocalDateTime) val); + if (val instanceof Timestamp) return (Timestamp) val; + throw new IllegalArgumentException("Cannot convert to DATETIME: " + val); + })); + + // TIMESTAMP + register( + new TypeDescriptor<>( + Types.TIMESTAMP, + Timestamp.class, + StandardSQLTypeName.TIMESTAMP, + Arrays.asList( + Timestamp.class, OffsetDateTime.class, Instant.class, ZonedDateTime.class), + (val, zone) -> { + if (val instanceof Timestamp) return (Timestamp) val; + throw new IllegalArgumentException("Cannot convert to TIMESTAMP: " + val); + })); + + // TIME + register( + new TypeDescriptor<>( + Types.TIME, + Time.class, + StandardSQLTypeName.TIME, + Arrays.asList(Time.class, LocalTime.class), + (val, zone) -> { + if (val instanceof Time) return (Time) val; + if (val instanceof LocalTime) return Time.valueOf((LocalTime) val); + throw new IllegalArgumentException("Cannot convert to TIME: " + val); + })); + + // BYTES + register( + new TypeDescriptor<>( + Types.VARBINARY, + byte[].class, + StandardSQLTypeName.BYTES, + Arrays.asList(byte[].class), + (val, zone) -> { + if (val instanceof byte[]) return (byte[]) val; + throw new IllegalArgumentException("Cannot convert to BYTES: " + val); + })); + + // ARRAY + register( + new TypeDescriptor<>( + Types.ARRAY, + Array.class, + StandardSQLTypeName.ARRAY, + Arrays.asList(Array.class), + (val, zone) -> { + if (val instanceof Array) return (Array) val; + throw new IllegalArgumentException("Cannot convert to ARRAY: " + val); + })); + + // STRUCT + register( + new TypeDescriptor<>( + Types.STRUCT, + Struct.class, + StandardSQLTypeName.STRUCT, + Arrays.asList(Struct.class), + (val, zone) -> { + if (val instanceof Struct) return (Struct) val; + throw new IllegalArgumentException("Cannot convert to STRUCT: " + val); + })); + + // JSON + register( + new TypeDescriptor<>( + Types.OTHER, + String.class, + StandardSQLTypeName.JSON, + Arrays.asList(com.google.gson.JsonObject.class), + (val, zone) -> String.valueOf(val))); + + // BIGNUMERIC + register( + new TypeDescriptor<>( + Types.NUMERIC, + BigDecimal.class, + StandardSQLTypeName.BIGNUMERIC, + Arrays.asList(BigDecimal.class), + (val, zone) -> { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + if (val instanceof String) return new BigDecimal((String) val); + throw new IllegalArgumentException("Cannot convert to BIGNUMERIC: " + val); + })); + + // GEOGRAPHY + register( + new TypeDescriptor<>( + Types.OTHER, + String.class, + StandardSQLTypeName.GEOGRAPHY, + Arrays.asList(String.class), + (val, zone) -> String.valueOf(val))); + + // INTERVAL + register( + new TypeDescriptor<>( + Types.OTHER, + String.class, + StandardSQLTypeName.INTERVAL, + Arrays.asList(String.class), + (val, zone) -> String.valueOf(val))); + + // RANGE + register( + new TypeDescriptor<>( + Types.OTHER, + String.class, + StandardSQLTypeName.RANGE, + Arrays.asList(String.class), + (val, zone) -> String.valueOf(val))); + } + + private static void register(TypeDescriptor descriptor) { + if (DESCRIPTORS_BY_ORDINAL[descriptor.getBqType().ordinal()] == null) { + DESCRIPTORS_BY_ORDINAL[descriptor.getBqType().ordinal()] = descriptor; + } + DESCRIPTORS_BY_JDBC_TYPE.putIfAbsent(descriptor.getJdbcType(), descriptor); + for (Class clazz : descriptor.getSupportedJavaTypes()) { + DESCRIPTORS_BY_CLASS.putIfAbsent(clazz, descriptor); + } + } + + private BigQueryTypeRegistry() {} + + /** + * Returns the exact BigQuery StandardSQLTypeName for a given Java class. If no mapping is found, + * returns StandardSQLTypeName.STRING as a fallback. + */ + public static StandardSQLTypeName toBigQueryType(Class clazz) { + TypeDescriptor descriptor = getDescriptorForClass(clazz); + if (descriptor != null) { + return descriptor.getBqType(); + } + return StandardSQLTypeName.STRING; + } + + /** Returns the default Java target class for a given JDBC type constant. */ + public static Class toJavaClass(int jdbcType) { + TypeDescriptor descriptor = DESCRIPTORS_BY_JDBC_TYPE.get(jdbcType); + if (descriptor != null) { + return descriptor.getDefaultJavaClass(); + } + return String.class; // fallback + } + + /** Converts the input value to the target class type. */ + @SuppressWarnings("unchecked") + public static T convert(Object input, Class targetClass) { + if (input == null) { + return null; + } + TypeDescriptor descriptor = getDescriptorForClass(targetClass); + if (descriptor == null) { + throw new IllegalArgumentException("Unsupported target class: " + targetClass.getName()); + } + return (T) descriptor.convert(input, null); + } + + private static TypeDescriptor getDescriptorForClass(Class clazz) { + TypeDescriptor descriptor = DESCRIPTORS_BY_CLASS.get(clazz); + if (descriptor != null) { + return descriptor; + } + // Fallback logic for subclasses/interfaces + for (Map.Entry, TypeDescriptor> entry : DESCRIPTORS_BY_CLASS.entrySet()) { + if (entry.getKey().isAssignableFrom(clazz)) { + return entry.getValue(); + } + } + return null; + } +} diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java new file mode 100644 index 000000000000..5b83047f7515 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java @@ -0,0 +1,71 @@ +/* + * 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 com.google.cloud.bigquery.StandardSQLTypeName; +import java.time.ZoneId; +import java.util.List; +import java.util.function.BiFunction; + +/** + * Defines the bidirectional mapping between a JDBC SQL type, a default Java class, and a BigQuery + * StandardSQLTypeName. It also contains the coercion logic to convert raw values into the expected + * Java type. + */ +public final class TypeDescriptor { + private final int jdbcType; + private final Class defaultJavaClass; + private final StandardSQLTypeName bqType; + private final List> supportedJavaTypes; + private final BiFunction coercer; + + TypeDescriptor( + int jdbcType, + Class defaultJavaClass, + StandardSQLTypeName bqType, + List> supportedJavaTypes, + BiFunction coercer) { + this.jdbcType = jdbcType; + this.defaultJavaClass = defaultJavaClass; + this.bqType = bqType; + this.supportedJavaTypes = supportedJavaTypes; + this.coercer = coercer; + } + + public int getJdbcType() { + return jdbcType; + } + + public Class getDefaultJavaClass() { + return defaultJavaClass; + } + + public StandardSQLTypeName getBqType() { + return bqType; + } + + public List> getSupportedJavaTypes() { + return supportedJavaTypes; + } + + public T convert(Object value, ZoneId zoneId) { + if (value == null) { + return null; + } + return coercer.apply(value, zoneId); + } +} From efb8327438fd2a1f0e1314980e10d9289d0977b1 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 12:45:32 -0400 Subject: [PATCH 2/7] fix(bigquery-jdbc): restrict visibility to package-private --- .../com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java | 2 +- .../java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 863917154895..3fda24cbf7a5 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 @@ -39,7 +39,7 @@ * A central, bidirectional engine for resolving and coercing types between JDBC, Java, and * BigQuery. */ -public final class BigQueryTypeRegistry { +final class BigQueryTypeRegistry { private static final TypeDescriptor[] DESCRIPTORS_BY_ORDINAL; private static final Map, TypeDescriptor> DESCRIPTORS_BY_CLASS; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java index 5b83047f7515..b729523aadc2 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java @@ -26,7 +26,7 @@ * StandardSQLTypeName. It also contains the coercion logic to convert raw values into the expected * Java type. */ -public final class TypeDescriptor { +final class TypeDescriptor { private final int jdbcType; private final Class defaultJavaClass; private final StandardSQLTypeName bqType; From c23e0ced3c4383596b051522f36302d14952e72e Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 13:02:05 -0400 Subject: [PATCH 3/7] redesign TypeRegistry --- .../bigquery/jdbc/BigQueryTypeRegistry.java | 161 ++++++++++++------ .../cloud/bigquery/jdbc/TypeDescriptor.java | 16 +- 2 files changed, 116 insertions(+), 61 deletions(-) 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 3fda24cbf7a5..99b2df8accbc 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 @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; +import java.sql.SQLException; import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; @@ -31,9 +32,8 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.util.Arrays; -import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * A central, bidirectional engine for resolving and coercing types between JDBC, Java, and @@ -53,8 +53,8 @@ final class BigQueryTypeRegistry { } } DESCRIPTORS_BY_ORDINAL = new TypeDescriptor[maxOrdinal + 1]; - DESCRIPTORS_BY_CLASS = new IdentityHashMap<>(); - DESCRIPTORS_BY_JDBC_TYPE = new HashMap<>(); + DESCRIPTORS_BY_CLASS = new ConcurrentHashMap<>(); + DESCRIPTORS_BY_JDBC_TYPE = new ConcurrentHashMap<>(); // BOOL register( @@ -63,10 +63,10 @@ final class BigQueryTypeRegistry { Boolean.class, StandardSQLTypeName.BOOL, Arrays.asList(Boolean.class), - (val, zone) -> { - if (val instanceof Boolean) return (Boolean) val; + (val, targetClass, zone) -> { + if (val instanceof Boolean) return val; if (val instanceof String) return Boolean.parseBoolean((String) val); - throw new IllegalArgumentException("Cannot convert to BOOL: " + val); + throw new SQLException("Cannot convert to BOOL: " + val); })); // STRING @@ -76,7 +76,7 @@ final class BigQueryTypeRegistry { String.class, StandardSQLTypeName.STRING, Arrays.asList(String.class), - (val, zone) -> String.valueOf(val))); + (val, targetClass, zone) -> String.valueOf(val))); // INT64 register( @@ -85,10 +85,16 @@ final class BigQueryTypeRegistry { Long.class, StandardSQLTypeName.INT64, Arrays.asList(Long.class, Integer.class, Short.class, Byte.class), - (val, zone) -> { - if (val instanceof Number) return ((Number) val).longValue(); - if (val instanceof String) return Long.parseLong((String) val); - throw new IllegalArgumentException("Cannot convert to INT64: " + val); + (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 SQLException("Cannot convert to INT64: " + val); + + if (targetClass == Integer.class) return (int) longVal; + if (targetClass == Short.class) return (short) longVal; + if (targetClass == Byte.class) return (byte) longVal; + return longVal; })); // FLOAT64 @@ -98,10 +104,14 @@ final class BigQueryTypeRegistry { Double.class, StandardSQLTypeName.FLOAT64, Arrays.asList(Double.class, Float.class), - (val, zone) -> { - if (val instanceof Number) return ((Number) val).doubleValue(); - if (val instanceof String) return Double.parseDouble((String) val); - throw new IllegalArgumentException("Cannot convert to FLOAT64: " + val); + (val, targetClass, zone) -> { + double doubleVal; + if (val instanceof Number) doubleVal = ((Number) val).doubleValue(); + else if (val instanceof String) doubleVal = Double.parseDouble((String) val); + else throw new SQLException("Cannot convert to FLOAT64: " + val); + + if (targetClass == Float.class) return (float) doubleVal; + return doubleVal; })); // NUMERIC @@ -111,11 +121,11 @@ final class BigQueryTypeRegistry { BigDecimal.class, StandardSQLTypeName.NUMERIC, Arrays.asList(BigDecimal.class), - (val, zone) -> { - if (val instanceof BigDecimal) return (BigDecimal) val; + (val, targetClass, zone) -> { + if (val instanceof BigDecimal) return val; if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); if (val instanceof String) return new BigDecimal((String) val); - throw new IllegalArgumentException("Cannot convert to NUMERIC: " + val); + throw new SQLException("Cannot convert to NUMERIC: " + val); })); // DATE @@ -125,10 +135,15 @@ final class BigQueryTypeRegistry { Date.class, StandardSQLTypeName.DATE, Arrays.asList(Date.class, LocalDate.class), - (val, zone) -> { - if (val instanceof Date) return (Date) val; - if (val instanceof LocalDate) return Date.valueOf((LocalDate) val); - throw new IllegalArgumentException("Cannot convert to DATE: " + val); + (val, targetClass, zone) -> { + Date sqlDate; + if (val instanceof Date) sqlDate = (Date) val; + else if (val instanceof LocalDate) sqlDate = Date.valueOf((LocalDate) val); + else if (val instanceof String) sqlDate = Date.valueOf((String) val); + else throw new SQLException("Cannot convert to DATE: " + val); + + if (targetClass == LocalDate.class) return sqlDate.toLocalDate(); + return sqlDate; })); // DATETIME @@ -137,11 +152,22 @@ final class BigQueryTypeRegistry { Types.TIMESTAMP, Timestamp.class, StandardSQLTypeName.DATETIME, - Arrays.asList(LocalDateTime.class), - (val, zone) -> { - if (val instanceof LocalDateTime) return Timestamp.valueOf((LocalDateTime) val); - if (val instanceof Timestamp) return (Timestamp) val; - throw new IllegalArgumentException("Cannot convert to DATETIME: " + val); + Arrays.asList(Timestamp.class, LocalDateTime.class), + (val, targetClass, zone) -> { + Timestamp ts; + if (val instanceof Timestamp) ts = (Timestamp) val; + else if (val instanceof LocalDateTime) ts = Timestamp.valueOf((LocalDateTime) val); + else if (val instanceof String) { + String str = (String) val; + if (str.contains("T")) { + ts = Timestamp.valueOf(LocalDateTime.parse(str)); + } else { + ts = Timestamp.valueOf(str); + } + } else throw new SQLException("Cannot convert to DATETIME: " + val); + + if (targetClass == LocalDateTime.class) return ts.toLocalDateTime(); + return ts; })); // TIMESTAMP @@ -152,9 +178,24 @@ final class BigQueryTypeRegistry { StandardSQLTypeName.TIMESTAMP, Arrays.asList( Timestamp.class, OffsetDateTime.class, Instant.class, ZonedDateTime.class), - (val, zone) -> { - if (val instanceof Timestamp) return (Timestamp) val; - throw new IllegalArgumentException("Cannot convert to TIMESTAMP: " + val); + (val, targetClass, zone) -> { + Timestamp ts; + if (val instanceof Timestamp) ts = (Timestamp) val; + else if (val instanceof String) { + String str = (String) val; + if (str.contains("T") || str.contains("Z")) { + ts = Timestamp.from(Instant.parse(str)); + } else { + ts = Timestamp.valueOf(str); + } + } else throw new SQLException("Cannot convert to TIMESTAMP: " + val); + + if (targetClass == Instant.class) return ts.toInstant(); + 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; })); // TIME @@ -164,10 +205,15 @@ final class BigQueryTypeRegistry { Time.class, StandardSQLTypeName.TIME, Arrays.asList(Time.class, LocalTime.class), - (val, zone) -> { - if (val instanceof Time) return (Time) val; - if (val instanceof LocalTime) return Time.valueOf((LocalTime) val); - throw new IllegalArgumentException("Cannot convert to TIME: " + val); + (val, targetClass, zone) -> { + Time sqlTime; + if (val instanceof Time) sqlTime = (Time) val; + else if (val instanceof LocalTime) sqlTime = Time.valueOf((LocalTime) val); + else if (val instanceof String) sqlTime = Time.valueOf((String) val); + else throw new SQLException("Cannot convert to TIME: " + val); + + if (targetClass == LocalTime.class) return sqlTime.toLocalTime(); + return sqlTime; })); // BYTES @@ -177,9 +223,9 @@ final class BigQueryTypeRegistry { byte[].class, StandardSQLTypeName.BYTES, Arrays.asList(byte[].class), - (val, zone) -> { - if (val instanceof byte[]) return (byte[]) val; - throw new IllegalArgumentException("Cannot convert to BYTES: " + val); + (val, targetClass, zone) -> { + if (val instanceof byte[]) return val; + throw new SQLException("Cannot convert to BYTES: " + val); })); // ARRAY @@ -189,9 +235,9 @@ final class BigQueryTypeRegistry { Array.class, StandardSQLTypeName.ARRAY, Arrays.asList(Array.class), - (val, zone) -> { - if (val instanceof Array) return (Array) val; - throw new IllegalArgumentException("Cannot convert to ARRAY: " + val); + (val, targetClass, zone) -> { + if (val instanceof Array) return val; + throw new SQLException("Cannot convert to ARRAY: " + val); })); // STRUCT @@ -201,9 +247,9 @@ final class BigQueryTypeRegistry { Struct.class, StandardSQLTypeName.STRUCT, Arrays.asList(Struct.class), - (val, zone) -> { - if (val instanceof Struct) return (Struct) val; - throw new IllegalArgumentException("Cannot convert to STRUCT: " + val); + (val, targetClass, zone) -> { + if (val instanceof Struct) return val; + throw new SQLException("Cannot convert to STRUCT: " + val); })); // JSON @@ -213,7 +259,7 @@ final class BigQueryTypeRegistry { String.class, StandardSQLTypeName.JSON, Arrays.asList(com.google.gson.JsonObject.class), - (val, zone) -> String.valueOf(val))); + (val, targetClass, zone) -> String.valueOf(val))); // BIGNUMERIC register( @@ -222,11 +268,11 @@ final class BigQueryTypeRegistry { BigDecimal.class, StandardSQLTypeName.BIGNUMERIC, Arrays.asList(BigDecimal.class), - (val, zone) -> { - if (val instanceof BigDecimal) return (BigDecimal) val; + (val, targetClass, zone) -> { + if (val instanceof BigDecimal) return val; if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); if (val instanceof String) return new BigDecimal((String) val); - throw new IllegalArgumentException("Cannot convert to BIGNUMERIC: " + val); + throw new SQLException("Cannot convert to BIGNUMERIC: " + val); })); // GEOGRAPHY @@ -236,7 +282,7 @@ final class BigQueryTypeRegistry { String.class, StandardSQLTypeName.GEOGRAPHY, Arrays.asList(String.class), - (val, zone) -> String.valueOf(val))); + (val, targetClass, zone) -> String.valueOf(val))); // INTERVAL register( @@ -245,7 +291,7 @@ final class BigQueryTypeRegistry { String.class, StandardSQLTypeName.INTERVAL, Arrays.asList(String.class), - (val, zone) -> String.valueOf(val))); + (val, targetClass, zone) -> String.valueOf(val))); // RANGE register( @@ -254,7 +300,7 @@ final class BigQueryTypeRegistry { String.class, StandardSQLTypeName.RANGE, Arrays.asList(String.class), - (val, zone) -> String.valueOf(val))); + (val, targetClass, zone) -> String.valueOf(val))); } private static void register(TypeDescriptor descriptor) { @@ -292,15 +338,15 @@ public static Class toJavaClass(int jdbcType) { /** Converts the input value to the target class type. */ @SuppressWarnings("unchecked") - public static T convert(Object input, Class targetClass) { + public static T convert(Object input, Class targetClass) throws SQLException { if (input == null) { return null; } TypeDescriptor descriptor = getDescriptorForClass(targetClass); if (descriptor == null) { - throw new IllegalArgumentException("Unsupported target class: " + targetClass.getName()); + throw new SQLException("Unsupported target class: " + targetClass.getName()); } - return (T) descriptor.convert(input, null); + return (T) descriptor.convert(input, targetClass, null); } private static TypeDescriptor getDescriptorForClass(Class clazz) { @@ -308,10 +354,13 @@ private static TypeDescriptor getDescriptorForClass(Class clazz) { if (descriptor != null) { return descriptor; } - // Fallback logic for subclasses/interfaces + // Fallback logic for subclasses/interfaces (O(N) initial lookup) for (Map.Entry, TypeDescriptor> entry : DESCRIPTORS_BY_CLASS.entrySet()) { if (entry.getKey().isAssignableFrom(clazz)) { - return entry.getValue(); + TypeDescriptor matchedDescriptor = entry.getValue(); + // Cache the result in the ConcurrentHashMap to turn subsequent subclass lookups into O(1) + DESCRIPTORS_BY_CLASS.putIfAbsent(clazz, matchedDescriptor); + return matchedDescriptor; } } return null; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java index b729523aadc2..a752ea164a8b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java @@ -17,9 +17,9 @@ package com.google.cloud.bigquery.jdbc; import com.google.cloud.bigquery.StandardSQLTypeName; +import java.sql.SQLException; import java.time.ZoneId; import java.util.List; -import java.util.function.BiFunction; /** * Defines the bidirectional mapping between a JDBC SQL type, a default Java class, and a BigQuery @@ -27,18 +27,24 @@ * Java type. */ final class TypeDescriptor { + + @FunctionalInterface + interface TypeCoercer { + Object coerce(Object value, Class targetClass, ZoneId zoneId) throws SQLException; + } + private final int jdbcType; private final Class defaultJavaClass; private final StandardSQLTypeName bqType; private final List> supportedJavaTypes; - private final BiFunction coercer; + private final TypeCoercer coercer; TypeDescriptor( int jdbcType, Class defaultJavaClass, StandardSQLTypeName bqType, List> supportedJavaTypes, - BiFunction coercer) { + TypeCoercer coercer) { this.jdbcType = jdbcType; this.defaultJavaClass = defaultJavaClass; this.bqType = bqType; @@ -62,10 +68,10 @@ public List> getSupportedJavaTypes() { return supportedJavaTypes; } - public T convert(Object value, ZoneId zoneId) { + public Object convert(Object value, Class targetClass, ZoneId zoneId) throws SQLException { if (value == null) { return null; } - return coercer.apply(value, zoneId); + return coercer.coerce(value, targetClass, zoneId); } } From 657852f8ffb99eaaf5c1a22ed93bbe33166de256 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 13:19:51 -0400 Subject: [PATCH 4/7] fix(bigquery-jdbc): use BigQueryJdbcException instead of generic SQLException --- .../bigquery/jdbc/BigQueryTypeRegistry.java | 30 +++++++++---------- .../cloud/bigquery/jdbc/TypeDescriptor.java | 6 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) 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 99b2df8accbc..145c549c8a34 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 @@ -20,7 +20,7 @@ import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; -import java.sql.SQLException; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; @@ -66,7 +66,7 @@ final class BigQueryTypeRegistry { (val, targetClass, zone) -> { if (val instanceof Boolean) return val; if (val instanceof String) return Boolean.parseBoolean((String) val); - throw new SQLException("Cannot convert to BOOL: " + val); + throw new BigQueryJdbcException("Cannot convert to BOOL: " + val); })); // STRING @@ -89,7 +89,7 @@ final class BigQueryTypeRegistry { long longVal; if (val instanceof Number) longVal = ((Number) val).longValue(); else if (val instanceof String) longVal = Long.parseLong((String) val); - else throw new SQLException("Cannot convert to INT64: " + val); + else throw new BigQueryJdbcException("Cannot convert to INT64: " + val); if (targetClass == Integer.class) return (int) longVal; if (targetClass == Short.class) return (short) longVal; @@ -108,7 +108,7 @@ final class BigQueryTypeRegistry { double doubleVal; if (val instanceof Number) doubleVal = ((Number) val).doubleValue(); else if (val instanceof String) doubleVal = Double.parseDouble((String) val); - else throw new SQLException("Cannot convert to FLOAT64: " + val); + else throw new BigQueryJdbcException("Cannot convert to FLOAT64: " + val); if (targetClass == Float.class) return (float) doubleVal; return doubleVal; @@ -125,7 +125,7 @@ final class BigQueryTypeRegistry { if (val instanceof BigDecimal) return val; if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); if (val instanceof String) return new BigDecimal((String) val); - throw new SQLException("Cannot convert to NUMERIC: " + val); + throw new BigQueryJdbcException("Cannot convert to NUMERIC: " + val); })); // DATE @@ -140,7 +140,7 @@ final class BigQueryTypeRegistry { if (val instanceof Date) sqlDate = (Date) val; else if (val instanceof LocalDate) sqlDate = Date.valueOf((LocalDate) val); else if (val instanceof String) sqlDate = Date.valueOf((String) val); - else throw new SQLException("Cannot convert to DATE: " + val); + else throw new BigQueryJdbcException("Cannot convert to DATE: " + val); if (targetClass == LocalDate.class) return sqlDate.toLocalDate(); return sqlDate; @@ -164,7 +164,7 @@ else if (val instanceof String) { } else { ts = Timestamp.valueOf(str); } - } else throw new SQLException("Cannot convert to DATETIME: " + val); + } else throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); if (targetClass == LocalDateTime.class) return ts.toLocalDateTime(); return ts; @@ -188,7 +188,7 @@ else if (val instanceof String) { } else { ts = Timestamp.valueOf(str); } - } else throw new SQLException("Cannot convert to TIMESTAMP: " + val); + } else throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); if (targetClass == Instant.class) return ts.toInstant(); if (targetClass == OffsetDateTime.class) @@ -210,7 +210,7 @@ else if (val instanceof String) { if (val instanceof Time) sqlTime = (Time) val; else if (val instanceof LocalTime) sqlTime = Time.valueOf((LocalTime) val); else if (val instanceof String) sqlTime = Time.valueOf((String) val); - else throw new SQLException("Cannot convert to TIME: " + val); + else throw new BigQueryJdbcException("Cannot convert to TIME: " + val); if (targetClass == LocalTime.class) return sqlTime.toLocalTime(); return sqlTime; @@ -225,7 +225,7 @@ else if (val instanceof String) { Arrays.asList(byte[].class), (val, targetClass, zone) -> { if (val instanceof byte[]) return val; - throw new SQLException("Cannot convert to BYTES: " + val); + throw new BigQueryJdbcException("Cannot convert to BYTES: " + val); })); // ARRAY @@ -237,7 +237,7 @@ else if (val instanceof String) { Arrays.asList(Array.class), (val, targetClass, zone) -> { if (val instanceof Array) return val; - throw new SQLException("Cannot convert to ARRAY: " + val); + throw new BigQueryJdbcException("Cannot convert to ARRAY: " + val); })); // STRUCT @@ -249,7 +249,7 @@ else if (val instanceof String) { Arrays.asList(Struct.class), (val, targetClass, zone) -> { if (val instanceof Struct) return val; - throw new SQLException("Cannot convert to STRUCT: " + val); + throw new BigQueryJdbcException("Cannot convert to STRUCT: " + val); })); // JSON @@ -272,7 +272,7 @@ else if (val instanceof String) { if (val instanceof BigDecimal) return val; if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); if (val instanceof String) return new BigDecimal((String) val); - throw new SQLException("Cannot convert to BIGNUMERIC: " + val); + throw new BigQueryJdbcException("Cannot convert to BIGNUMERIC: " + val); })); // GEOGRAPHY @@ -338,13 +338,13 @@ public static Class toJavaClass(int jdbcType) { /** Converts the input value to the target class type. */ @SuppressWarnings("unchecked") - public static T convert(Object input, Class targetClass) throws SQLException { + public static T convert(Object input, Class targetClass) throws BigQueryJdbcException { if (input == null) { return null; } TypeDescriptor descriptor = getDescriptorForClass(targetClass); if (descriptor == null) { - throw new SQLException("Unsupported target class: " + targetClass.getName()); + throw new BigQueryJdbcException("Unsupported target class: " + targetClass.getName()); } return (T) descriptor.convert(input, targetClass, null); } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java index a752ea164a8b..ac53be26fb36 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java @@ -17,7 +17,7 @@ package com.google.cloud.bigquery.jdbc; import com.google.cloud.bigquery.StandardSQLTypeName; -import java.sql.SQLException; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; import java.time.ZoneId; import java.util.List; @@ -30,7 +30,7 @@ final class TypeDescriptor { @FunctionalInterface interface TypeCoercer { - Object coerce(Object value, Class targetClass, ZoneId zoneId) throws SQLException; + Object coerce(Object value, Class targetClass, ZoneId zoneId) throws BigQueryJdbcException; } private final int jdbcType; @@ -68,7 +68,7 @@ public List> getSupportedJavaTypes() { return supportedJavaTypes; } - public Object convert(Object value, Class targetClass, ZoneId zoneId) throws SQLException { + public Object convert(Object value, Class targetClass, ZoneId zoneId) throws BigQueryJdbcException { if (value == null) { return null; } From 62800cfc21d5ef35101972467cf0b287ed4eb68d Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 13:20:35 -0400 Subject: [PATCH 5/7] lint --- .../com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java | 2 +- .../java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 145c549c8a34..1d569fa5d6c0 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 @@ -17,10 +17,10 @@ package com.google.cloud.bigquery.jdbc; import com.google.cloud.bigquery.StandardSQLTypeName; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; -import com.google.cloud.bigquery.exception.BigQueryJdbcException; import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java index ac53be26fb36..56d161b11b6d 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/TypeDescriptor.java @@ -68,7 +68,8 @@ public List> getSupportedJavaTypes() { return supportedJavaTypes; } - public Object convert(Object value, Class targetClass, ZoneId zoneId) throws BigQueryJdbcException { + public Object convert(Object value, Class targetClass, ZoneId zoneId) + throws BigQueryJdbcException { if (value == null) { return null; } From 94fd2936adef5126891d19c3b430841331e5a255 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 14:13:33 -0400 Subject: [PATCH 6/7] timeZoneUtility --- .../jdbc/BigQueryTimezoneUtility.java | 79 +++++++++++++++++++ .../bigquery/jdbc/BigQueryTypeRegistry.java | 45 ++++++----- 2 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTimezoneUtility.java diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTimezoneUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTimezoneUtility.java new file mode 100644 index 000000000000..551667a1b450 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTimezoneUtility.java @@ -0,0 +1,79 @@ +/* + * 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 java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; + +/** + * A highly optimized utility for bridging BigQuery's civil time and absolute time semantics to + * legacy JDBC Date/Time/Timestamp classes using JSR-310 timezone anchoring. + */ +final class BigQueryTimezoneUtility { + + private BigQueryTimezoneUtility() {} + + /** + * Converts a BigQuery civil DATETIME string into an absolute Timestamp by anchoring it to the + * provided timezone (or JVM default if null). + */ + public static Timestamp boxDateTime(String val, ZoneId zoneId) { + ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault(); + String isoString = val.replace(' ', 'T'); + return Timestamp.from(LocalDateTime.parse(isoString).atZone(targetZone).toInstant()); + } + + /** + * Converts a BigQuery civil DATE string into an absolute Date by anchoring it to midnight of the + * provided timezone (or JVM default if null). + */ + public static Date boxDate(String val, ZoneId zoneId) { + ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault(); + return new Date(LocalDate.parse(val).atStartOfDay(targetZone).toInstant().toEpochMilli()); + } + + /** + * Converts a BigQuery civil TIME string into an absolute Time by anchoring it to Jan 1, 1970 of + * the provided timezone (or JVM default if null). + */ + public static Time boxTime(String val, ZoneId zoneId) { + ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault(); + return new Time( + LocalTime.parse(val) + .atDate(LocalDate.of(1970, 1, 1)) + .atZone(targetZone) + .toInstant() + .toEpochMilli()); + } + + /** + * Converts a BigQuery absolute TIMESTAMP string into a legacy Timestamp. Because it is absolute, + * the Calendar timezone is explicitly ignored per JDBC 4.2 spec. + */ + public static Timestamp boxTimestamp(String val) { + if (val.contains("T") || val.contains("Z")) { + return Timestamp.from(Instant.parse(val)); + } + return Timestamp.valueOf(val); + } +} 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 1d569fa5d6c0..f3be4066e046 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 @@ -123,7 +123,7 @@ final class BigQueryTypeRegistry { Arrays.asList(BigDecimal.class), (val, targetClass, zone) -> { if (val instanceof BigDecimal) return val; - if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + if (val instanceof Number) return new BigDecimal(val.toString()); if (val instanceof String) return new BigDecimal((String) val); throw new BigQueryJdbcException("Cannot convert to NUMERIC: " + val); })); @@ -138,8 +138,11 @@ final class BigQueryTypeRegistry { (val, targetClass, zone) -> { Date sqlDate; if (val instanceof Date) sqlDate = (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) sqlDate = Date.valueOf((String) val); + else if (val instanceof String) + sqlDate = BigQueryTimezoneUtility.boxDate((String) val, zone); else throw new BigQueryJdbcException("Cannot convert to DATE: " + val); if (targetClass == LocalDate.class) return sqlDate.toLocalDate(); @@ -156,15 +159,12 @@ final class BigQueryTypeRegistry { (val, targetClass, zone) -> { Timestamp ts; 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) { - String str = (String) val; - if (str.contains("T")) { - ts = Timestamp.valueOf(LocalDateTime.parse(str)); - } else { - ts = Timestamp.valueOf(str); - } - } else throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); + else if (val instanceof String) + ts = BigQueryTimezoneUtility.boxDateTime((String) val, zone); + else throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); if (targetClass == LocalDateTime.class) return ts.toLocalDateTime(); return ts; @@ -181,14 +181,16 @@ else if (val instanceof String) { (val, targetClass, zone) -> { Timestamp ts; if (val instanceof Timestamp) ts = (Timestamp) val; - else if (val instanceof String) { - String str = (String) val; - if (str.contains("T") || str.contains("Z")) { - ts = Timestamp.from(Instant.parse(str)); - } else { - ts = Timestamp.valueOf(str); - } - } else throw new BigQueryJdbcException("Cannot convert to 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) + ts = Timestamp.from(((OffsetDateTime) val).toInstant()); + else if (val instanceof ZonedDateTime) + ts = Timestamp.from(((ZonedDateTime) val).toInstant()); + else if (val instanceof String) + ts = BigQueryTimezoneUtility.boxTimestamp((String) val); + else throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); if (targetClass == Instant.class) return ts.toInstant(); if (targetClass == OffsetDateTime.class) @@ -208,8 +210,11 @@ else if (val instanceof String) { (val, targetClass, zone) -> { Time sqlTime; if (val instanceof Time) sqlTime = (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) sqlTime = Time.valueOf((String) val); + else if (val instanceof String) + sqlTime = BigQueryTimezoneUtility.boxTime((String) val, zone); else throw new BigQueryJdbcException("Cannot convert to TIME: " + val); if (targetClass == LocalTime.class) return sqlTime.toLocalTime(); @@ -270,7 +275,7 @@ else if (val instanceof String) { Arrays.asList(BigDecimal.class), (val, targetClass, zone) -> { if (val instanceof BigDecimal) return val; - if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + if (val instanceof Number) return new BigDecimal(val.toString()); if (val instanceof String) return new BigDecimal((String) val); throw new BigQueryJdbcException("Cannot convert to BIGNUMERIC: " + val); })); From 536df8b32dd8e68f594e23ebd63cdff67d61ff16 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 29 Jul 2026 15:45:13 -0400 Subject: [PATCH 7/7] feat: core type registry matrix (Phase 1) --- .../bigquery/jdbc/BigQueryTypeRegistry.java | 132 +++++------------- 1 file changed, 38 insertions(+), 94 deletions(-) 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 f3be4066e046..01de760a9f35 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 @@ -128,99 +128,10 @@ final class BigQueryTypeRegistry { throw new BigQueryJdbcException("Cannot convert to NUMERIC: " + val); })); - // DATE - register( - new TypeDescriptor<>( - Types.DATE, - Date.class, - StandardSQLTypeName.DATE, - Arrays.asList(Date.class, LocalDate.class), - (val, targetClass, zone) -> { - Date sqlDate; - if (val instanceof Date) sqlDate = (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) - sqlDate = BigQueryTimezoneUtility.boxDate((String) val, zone); - else throw new BigQueryJdbcException("Cannot convert to DATE: " + val); - - if (targetClass == LocalDate.class) return sqlDate.toLocalDate(); - return sqlDate; - })); - - // DATETIME - register( - new TypeDescriptor<>( - Types.TIMESTAMP, - Timestamp.class, - StandardSQLTypeName.DATETIME, - Arrays.asList(Timestamp.class, LocalDateTime.class), - (val, targetClass, zone) -> { - Timestamp ts; - 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) - ts = BigQueryTimezoneUtility.boxDateTime((String) val, zone); - else throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); - - if (targetClass == LocalDateTime.class) return ts.toLocalDateTime(); - return ts; - })); - - // TIMESTAMP - register( - new TypeDescriptor<>( - Types.TIMESTAMP, - Timestamp.class, - StandardSQLTypeName.TIMESTAMP, - Arrays.asList( - Timestamp.class, OffsetDateTime.class, Instant.class, ZonedDateTime.class), - (val, targetClass, zone) -> { - Timestamp ts; - 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) - ts = Timestamp.from(((OffsetDateTime) val).toInstant()); - else if (val instanceof ZonedDateTime) - ts = Timestamp.from(((ZonedDateTime) val).toInstant()); - else if (val instanceof String) - ts = BigQueryTimezoneUtility.boxTimestamp((String) val); - else throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val); - - if (targetClass == Instant.class) return ts.toInstant(); - 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; - })); - - // TIME - register( - new TypeDescriptor<>( - Types.TIME, - Time.class, - StandardSQLTypeName.TIME, - Arrays.asList(Time.class, LocalTime.class), - (val, targetClass, zone) -> { - Time sqlTime; - if (val instanceof Time) sqlTime = (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) - sqlTime = BigQueryTimezoneUtility.boxTime((String) val, zone); - else throw new BigQueryJdbcException("Cannot convert to TIME: " + val); - - if (targetClass == LocalTime.class) return sqlTime.toLocalTime(); - return sqlTime; - })); - + // --- TEMPORAL TYPES MOVED TO PHASE 2 PR --- + // DATE, DATETIME, TIMESTAMP, and TIME descriptors require BigQueryTemporalUtility + // which will be introduced in the stacked Phase 2 PR. + // BYTES register( new TypeDescriptor<>( @@ -341,7 +252,9 @@ public static Class toJavaClass(int jdbcType) { return String.class; // fallback } - /** Converts the input value to the target class type. */ + /** + * Converts the input value to the target class type by looking up the target class descriptor. + */ @SuppressWarnings("unchecked") public static T convert(Object input, Class targetClass) throws BigQueryJdbcException { if (input == null) { @@ -354,6 +267,37 @@ public static T convert(Object input, Class targetClass) throws BigQueryJ return (T) descriptor.convert(input, targetClass, null); } + /** + * High-performance hotpath convert for ResultSets. Converts the input value using the default + * mapping for the given BigQuery type via O(1) array indexing. + */ + public static Object convert(Object input, StandardSQLTypeName bqType, ZoneId zoneId) + throws BigQueryJdbcException { + if (input == null) return null; + 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); + } + TypeDescriptor descriptor = DESCRIPTORS_BY_ORDINAL[ordinal]; + return descriptor.convert(input, descriptor.getDefaultJavaClass(), zoneId); + } + + /** + * High-performance hotpath convert for ResultSets. Converts the input value to the target class + * using the descriptor for the given BigQuery type via O(1) array indexing. + */ + @SuppressWarnings("unchecked") + public static T convert( + Object input, StandardSQLTypeName bqType, Class targetClass, ZoneId zoneId) + throws BigQueryJdbcException { + if (input == null) return null; + 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); + } + return (T) DESCRIPTORS_BY_ORDINAL[ordinal].convert(input, targetClass, zoneId); + } + private static TypeDescriptor getDescriptorForClass(Class clazz) { TypeDescriptor descriptor = DESCRIPTORS_BY_CLASS.get(clazz); if (descriptor != null) {