refactor(jdbc): migrate write path and metadata to TypeRegistry - #14061
refactor(jdbc): migrate write path and metadata to TypeRegistry#14061Neenu1995 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the BigQuery JDBC driver by migrating type mapping logic from BigQueryJdbcTypeMappings to BigQueryTypeRegistry across several classes, including BigQueryCallableStatement, BigQueryParameterMetaData, and BigQueryResultSetMetadata. The review feedback highlights several critical improvement opportunities: correcting misplaced Javadoc comments in BigQueryTypeRegistry, resolving dead code caused by a redundant null check on a primitive return value in BigQueryParameterMetaData, and addressing lossy type conversions where converting to a JDBC type first before mapping to a Java class loses precision for types like GEOGRAPHY or JSON. It is recommended to introduce a direct toJavaClass(StandardSQLTypeName) method to preserve type safety and prevent potential null pointer exceptions.
| /** Returns the default Java target class for a given JDBC type constant. */ | ||
| /** Returns the JDBC Type constant for a given BigQuery type. */ | ||
| public static int toJdbcType(StandardSQLTypeName bqType) { | ||
| if (bqType == null) return java.sql.Types.OTHER; | ||
| int ordinal = bqType.ordinal(); | ||
| if (ordinal >= DESCRIPTORS_BY_ORDINAL.length || DESCRIPTORS_BY_ORDINAL[ordinal] == null) { | ||
| return java.sql.Types.OTHER; | ||
| } | ||
| return DESCRIPTORS_BY_ORDINAL[ordinal].getJdbcType(); | ||
| } | ||
|
|
||
| public static Class<?> toJavaClass(int jdbcType) { |
There was a problem hiding this comment.
The Javadoc comments here are misplaced. The Javadoc for toJavaClass(int) (/** Returns the default Java target class for a given JDBC type constant. */) has been left above toJdbcType, resulting in double Javadocs for toJdbcType and none for toJavaClass(int).
Additionally, we can introduce a non-lossy toJavaClass(StandardSQLTypeName) method to avoid converting StandardSQLTypeName to a JDBC int type first (which loses precision for types like GEOGRAPHY, JSON, INTERVAL, and RANGE that all map to Types.OTHER).
/** Returns the JDBC Type constant for a given BigQuery type. */
public static int toJdbcType(StandardSQLTypeName bqType) {
if (bqType == null) return Types.OTHER;
int ordinal = bqType.ordinal();
if (ordinal >= DESCRIPTORS_BY_ORDINAL.length || DESCRIPTORS_BY_ORDINAL[ordinal] == null) {
return Types.OTHER;
}
return DESCRIPTORS_BY_ORDINAL[ordinal].getJdbcType();
}
/** Returns the default Java target class for a given BigQuery type. */
public static Class<?> toJavaClass(StandardSQLTypeName bqType) {
if (bqType == null) return null;
int ordinal = bqType.ordinal();
if (ordinal >= DESCRIPTORS_BY_ORDINAL.length || DESCRIPTORS_BY_ORDINAL[ordinal] == null) {
return null;
}
return DESCRIPTORS_BY_ORDINAL[ordinal].getJavaClass();
}
/** Returns the default Java target class for a given JDBC type constant. */
public static Class<?> toJavaClass(int jdbcType) {References
- Update stale Javadoc comments to reflect new method behavior for improved clarity, rather than removing them.
- Do not use fully qualified class names (such as java.time.Duration) if there is no class name conflict in the file and the class is already imported, as it unnecessarily reduces code readability.
| StandardSQLTypeName sqlType = getStandardSQLTypeName(param); | ||
| if (sqlType != null) { | ||
| Class<?> clazz = BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get(sqlType); | ||
| Class<?> clazz = BigQueryTypeRegistry.toJavaClass(BigQueryTypeRegistry.toJdbcType(sqlType)); |
There was a problem hiding this comment.
Using BigQueryTypeRegistry.toJavaClass(BigQueryTypeRegistry.toJdbcType(sqlType)) is lossy because multiple distinct BigQuery types (such as GEOGRAPHY, JSON, INTERVAL, and RANGE) all map to Types.OTHER. Converting them to a JDBC type first and then to a Java class loses the specific type information and can return null or incorrect classes.
Instead, use the direct toJavaClass(StandardSQLTypeName) method.
| Class<?> clazz = BigQueryTypeRegistry.toJavaClass(BigQueryTypeRegistry.toJdbcType(sqlType)); | |
| Class<?> clazz = BigQueryTypeRegistry.toJavaClass(sqlType); |
| return BigQueryTypeRegistry.toJavaClass( | ||
| BigQueryTypeRegistry.toJdbcType(getStandardSQLTypeName(column))) | ||
| .getName(); |
There was a problem hiding this comment.
Similarly, using BigQueryTypeRegistry.toJavaClass(BigQueryTypeRegistry.toJdbcType(...)) is lossy and can lead to NullPointerException if toJavaClass returns null.
Instead, use the direct toJavaClass(StandardSQLTypeName) method and add a safe null check.
Class<?> clazz = BigQueryTypeRegistry.toJavaClass(getStandardSQLTypeName(column));
return clazz != null ? clazz.getName() : null;| if (sqlType == null) { | ||
| return Types.OTHER; | ||
| } | ||
| Integer jdbcType = BigQueryJdbcTypeMappings.standardSQLToJavaSqlTypesMapping.get(sqlType); | ||
| Integer jdbcType = BigQueryTypeRegistry.toJdbcType(sqlType); | ||
| if (jdbcType != null) { | ||
| return jdbcType; | ||
| } |
There was a problem hiding this comment.
Since BigQueryTypeRegistry.toJdbcType(sqlType) returns a primitive int, the local variable jdbcType (autoboxed to Integer) can never be null. The null check if (jdbcType != null) is dead code.
Furthermore, toJdbcType(null) already returns Types.OTHER by default, so we can simplify this entire block to a single return statement.
return BigQueryTypeRegistry.toJdbcType(sqlType);|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the BigQuery JDBC driver's type mapping mechanism by migrating from BigQueryJdbcTypeMappings to a centralized BigQueryTypeRegistry. This change affects several classes, including BigQueryCallableStatement, BigQueryParameterMetaData, and BigQueryResultSetMetadata. The review feedback highlights several areas for improvement: first, expanding the JDBC_TO_JAVA_CLASS_MAP to include missing standard JDBC types (such as DECIMAL and CHAR) to prevent runtime exceptions; and second, removing redundant null checks and boxing operations across the codebase, as the newly introduced registry methods either return primitive types or are guaranteed to return non-null values.
| private static final Map<Integer, Class<?>> JDBC_TO_JAVA_CLASS_MAP = | ||
| ImmutableMap.<Integer, Class<?>>builder() | ||
| .put(Types.BIGINT, Long.class) | ||
| .put(Types.INTEGER, Integer.class) | ||
| .put(Types.SMALLINT, Short.class) | ||
| .put(Types.TINYINT, Byte.class) | ||
| .put(Types.BOOLEAN, Boolean.class) | ||
| .put(Types.DOUBLE, Double.class) | ||
| .put(Types.FLOAT, Float.class) | ||
| .put(Types.NUMERIC, BigDecimal.class) | ||
| .put(Types.VARCHAR, String.class) | ||
| .put(Types.NVARCHAR, String.class) | ||
| .put(Types.TIMESTAMP, Timestamp.class) | ||
| .put(Types.DATE, Date.class) | ||
| .put(Types.TIME, Time.class) | ||
| .put(Types.OTHER, String.class) | ||
| .put(Types.BINARY, byte[].class) | ||
| .put(Types.VARBINARY, byte[].class) | ||
| .put(Types.STRUCT, Struct.class) | ||
| .put(Types.BIT, Boolean.class) | ||
| .put(Types.ARRAY, Array.class) | ||
| .put(Types.NULL, String.class) | ||
| .build(); |
There was a problem hiding this comment.
The JDBC_TO_JAVA_CLASS_MAP is missing several common JDBC type mappings, such as Types.DECIMAL (which should map to BigDecimal.class), Types.CHAR (which should map to String.class), and potentially Types.REAL or Types.CLOB. Without these mappings, calling methods like setNull or registerOutParameter with these standard JDBC types will throw a BigQueryJdbcSqlFeatureNotSupportedException at runtime, which could break compatibility with existing ORMs or database tools.
private static final Map<Integer, Class<?>> JDBC_TO_JAVA_CLASS_MAP =
ImmutableMap.<Integer, Class<?>>builder()
.put(Types.BIGINT, Long.class)
.put(Types.INTEGER, Integer.class)
.put(Types.SMALLINT, Short.class)
.put(Types.TINYINT, Byte.class)
.put(Types.BOOLEAN, Boolean.class)
.put(Types.DOUBLE, Double.class)
.put(Types.FLOAT, Float.class)
.put(Types.REAL, Float.class)
.put(Types.NUMERIC, BigDecimal.class)
.put(Types.DECIMAL, BigDecimal.class)
.put(Types.CHAR, String.class)
.put(Types.VARCHAR, String.class)
.put(Types.LONGVARCHAR, String.class)
.put(Types.NCHAR, String.class)
.put(Types.NVARCHAR, String.class)
.put(Types.LONGNVARCHAR, String.class)
.put(Types.CLOB, String.class)
.put(Types.TIMESTAMP, Timestamp.class)
.put(Types.DATE, Date.class)
.put(Types.TIME, Time.class)
.put(Types.OTHER, String.class)
.put(Types.BINARY, byte[].class)
.put(Types.VARBINARY, byte[].class)
.put(Types.LONGVARBINARY, byte[].class)
.put(Types.STRUCT, Struct.class)
.put(Types.BIT, Boolean.class)
.put(Types.ARRAY, Array.class)
.put(Types.NULL, String.class)
.build();| Class<?> javaType = BigQueryTypeRegistry.toJavaClass(sqlType); | ||
| if (javaType == null) { | ||
| javaType = String.class; | ||
| } |
There was a problem hiding this comment.
Since BigQueryTypeRegistry.toJavaClass(int) throws a BigQueryJdbcSqlFeatureNotSupportedException when the JDBC type is unsupported instead of returning null, the variable javaType will never be null. The subsequent null check is dead code and can be safely removed.
| Class<?> javaType = BigQueryTypeRegistry.toJavaClass(sqlType); | |
| if (javaType == null) { | |
| javaType = String.class; | |
| } | |
| Class<?> javaType = BigQueryTypeRegistry.toJavaClass(sqlType); |
References
- Avoid adding defensive null checks or redundant null-handling logic for method return values that are guaranteed to be non-null by design, such as when an empty state or failure condition would already throw an exception earlier in the execution path.
| Integer jdbcType = BigQueryTypeRegistry.toJdbcType(sqlType); | ||
| if (jdbcType != null) { | ||
| return jdbcType; |
There was a problem hiding this comment.
Since BigQueryTypeRegistry.toJdbcType(sqlType) returns a primitive int (which can never be null), the Integer boxing and subsequent null check are redundant. We can simplify this by checking against Types.OTHER (the fallback value returned by toJdbcType when unmapped) to ensure the rest of the method remains reachable.
| Integer jdbcType = BigQueryTypeRegistry.toJdbcType(sqlType); | |
| if (jdbcType != null) { | |
| return jdbcType; | |
| int jdbcType = BigQueryTypeRegistry.toJdbcType(sqlType); | |
| if (jdbcType != Types.OTHER) { | |
| return jdbcType; | |
| } |
References
- Avoid adding defensive null checks for values that are guaranteed to be non-null by design, as this can hide invariant breaks.
| Class<?> clazz = BigQueryTypeRegistry.toJavaClass(sqlType); | ||
| if (clazz != null) { | ||
| return clazz.getName(); | ||
| } |
There was a problem hiding this comment.
Since BigQueryTypeRegistry.toJavaClass(StandardSQLTypeName) always returns a non-null class (falling back to String.class if the type is unmapped or null), the null check if (clazz != null) is redundant and can be simplified.
| Class<?> clazz = BigQueryTypeRegistry.toJavaClass(sqlType); | |
| if (clazz != null) { | |
| return clazz.getName(); | |
| } | |
| return BigQueryTypeRegistry.toJavaClass(sqlType).getName(); |
References
- Avoid adding defensive null checks or redundant null-handling logic for method return values that are guaranteed to be non-null by design, such as when an empty state or failure condition would already throw an exception earlier in the execution path.
b/538177258
This PR executes Part 1 of Phase 4 of the Type Registry consolidation.
It migrates all parameter binding (Write Path) and metadata sizing away from the legacy `BigQueryJdbcTypeMappings` and onto the unified `BigQueryTypeRegistry`.
Changes: