Skip to content

Commit 51ba902

Browse files
authored
fix(bigquery-jdbc): fallback to standard precision and scale for numeric, integer, and temporal types (googleapis#13321)
b/516471484 ## Problem Currently, `ResultSetMetaData.getPrecision()` and `getScale()` fall back to `0` for all standard BigQuery types unless explicitly returned by the server's field metadata (which is rarely populated for basic columns). Returning `0` violates the JDBC specification for exact numeric, integer, and temporal types, which can cause ORMs and schema inspectors to misbehave. Additionally, the type mappings inside `BigQueryDatabaseMetaData` used for schema queries (like `getColumns()`) were inconsistent with the JDBC specification and standard string length representations for temporal types, reporting a precision of `29` with `null` scale for `TIMESTAMP` and `DATETIME`, and missing scale definitions for `TIME` and `DATE`. ### Solution 1. **`BigQueryResultSetMetadata`**: Modified BigQueryResultSetMetadata.java to fall back to the standard SQL type definition properties when explicit schema-defined precision/scale are absent. The driver now returns standard fallback values based on the column's StandardSQLTypeName: | BigQuery Type | JDBC SQL Type | Fallback Precision | Fallback Scale | Justification | | :--- | :--- | :--- | :--- | :--- | | **`INT64`** | `Types.BIGINT` | `19` | `0` | Signed 64-bit integer range limit | | **`NUMERIC`** | `Types.NUMERIC` | `38` | `9` | Standard BigQuery NUMERIC specifications | | **`BIGNUMERIC`** | `Types.NUMERIC` | `77` | `38` | Standard BigQuery BIGNUMERIC specifications | | **`DATE`** | `Types.DATE` | `10` | `0` | Display length of `"YYYY-MM-DD"` | | **`TIME`** | `Types.TIME` | `15` | `6` | Display length of `"HH:MM:SS.ffffff"` (microseconds) | | **`TIMESTAMP`** / **`DATETIME`** | `Types.TIMESTAMP` | `26` | `6` | Display length of `"YYYY-MM-DD HH:MM:SS.ffffff"` | | **`BOOL`** | `Types.BOOLEAN` | `1` | `0` | Single-digit bit/flag | | **`FLOAT64`** | `Types.DOUBLE` | `15` | `0` | Double-precision floating point precision limit | 2. **`BigQueryDatabaseMetaData`**: - Aligned `STANDARD_TYPE_INFO` mappings for `TIMESTAMP`, `DATETIME`, `DATE`, and `TIME` to match the fallback precision and scale values reported by `BigQueryResultSetMetadata`. - Added explanatory comments in the code justifying the fallback/precision values. ### Testing - Updated unit tests in BigQueryResultSetMetadataTest.java to verify correct fallback values across all 13 dataset columns.
1 parent 946dfb4 commit 51ba902

5 files changed

Lines changed: 163 additions & 182 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import com.google.cloud.bigquery.TableDefinition;
4343
import com.google.cloud.bigquery.TableId;
4444
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
45-
import com.google.common.collect.ImmutableMap;
45+
import com.google.cloud.bigquery.jdbc.BigQueryJdbcTypeMappings.ColumnTypeInfo;
4646
import java.io.BufferedReader;
4747
import java.io.IOException;
4848
import java.io.InputStream;
@@ -60,7 +60,6 @@
6060
import java.util.Comparator;
6161
import java.util.HashSet;
6262
import java.util.List;
63-
import java.util.Map;
6463
import java.util.Properties;
6564
import java.util.Scanner;
6665
import java.util.Set;
@@ -2436,27 +2435,6 @@ List<FieldValue> createColumnRow(
24362435
return values;
24372436
}
24382437

2439-
static class ColumnTypeInfo {
2440-
final int jdbcType;
2441-
final String typeName;
2442-
final Integer columnSize;
2443-
final Integer decimalDigits;
2444-
final Integer numPrecRadix;
2445-
2446-
ColumnTypeInfo(
2447-
int jdbcType,
2448-
String typeName,
2449-
Integer columnSize,
2450-
Integer decimalDigits,
2451-
Integer numPrecRadix) {
2452-
this.jdbcType = jdbcType;
2453-
this.typeName = typeName;
2454-
this.columnSize = columnSize;
2455-
this.decimalDigits = decimalDigits;
2456-
this.numPrecRadix = numPrecRadix;
2457-
}
2458-
}
2459-
24602438
ColumnTypeInfo mapBigQueryTypeToJdbc(Field field) {
24612439
Mode mode = (field.getMode() == null) ? Mode.NULLABLE : field.getMode();
24622440
if (mode == Mode.REPEATED) {
@@ -4833,53 +4811,12 @@ private Tuple<String, String> determineEffectiveCatalogAndSchema(
48334811
return Tuple.of(effectiveCatalog, effectiveSchemaPattern);
48344812
}
48354813

4836-
// BigQuery STRING represents Unicode character strings (UTF-8).
4837-
// Standard JDBC maps UTF-8/Unicode data to Types.NVARCHAR rather than Types.VARCHAR.
4838-
private static final Map<StandardSQLTypeName, ColumnTypeInfo> STANDARD_TYPE_INFO =
4839-
ImmutableMap.<StandardSQLTypeName, ColumnTypeInfo>builder()
4840-
.put(StandardSQLTypeName.INT64, new ColumnTypeInfo(Types.BIGINT, "INT64", 19, 0, 10))
4841-
.put(StandardSQLTypeName.BOOL, new ColumnTypeInfo(Types.BOOLEAN, "BOOL", 1, null, null))
4842-
.put(
4843-
StandardSQLTypeName.FLOAT64,
4844-
new ColumnTypeInfo(Types.DOUBLE, "FLOAT64", 15, null, 10))
4845-
.put(StandardSQLTypeName.NUMERIC, new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 38, 9, 10))
4846-
.put(
4847-
StandardSQLTypeName.BIGNUMERIC,
4848-
new ColumnTypeInfo(Types.NUMERIC, "BIGNUMERIC", 77, 38, 10))
4849-
.put(
4850-
StandardSQLTypeName.STRING,
4851-
new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null))
4852-
.put(
4853-
StandardSQLTypeName.TIMESTAMP,
4854-
new ColumnTypeInfo(Types.TIMESTAMP, "TIMESTAMP", 29, null, null))
4855-
.put(
4856-
StandardSQLTypeName.DATETIME,
4857-
new ColumnTypeInfo(Types.TIMESTAMP, "DATETIME", 29, null, null))
4858-
.put(StandardSQLTypeName.DATE, new ColumnTypeInfo(Types.DATE, "DATE", 10, null, null))
4859-
.put(StandardSQLTypeName.TIME, new ColumnTypeInfo(Types.TIME, "TIME", 15, null, null))
4860-
.put(
4861-
StandardSQLTypeName.GEOGRAPHY,
4862-
new ColumnTypeInfo(Types.OTHER, "GEOGRAPHY", null, null, null))
4863-
.put(StandardSQLTypeName.JSON, new ColumnTypeInfo(Types.OTHER, "JSON", null, null, null))
4864-
.put(
4865-
StandardSQLTypeName.INTERVAL,
4866-
new ColumnTypeInfo(Types.OTHER, "INTERVAL", null, null, null))
4867-
.put(
4868-
StandardSQLTypeName.RANGE, new ColumnTypeInfo(Types.OTHER, "RANGE", null, null, null))
4869-
.put(
4870-
StandardSQLTypeName.BYTES,
4871-
new ColumnTypeInfo(Types.VARBINARY, "BYTES", null, null, null))
4872-
.put(
4873-
StandardSQLTypeName.STRUCT,
4874-
new ColumnTypeInfo(Types.STRUCT, "STRUCT", null, null, null))
4875-
.build();
4876-
48774814
private ColumnTypeInfo getColumnTypeInfoForSqlType(StandardSQLTypeName bqType) {
48784815
if (bqType == null) {
48794816
LOG.warning("Null BigQuery type encountered. Mapping to STRING.");
48804817
return new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null);
48814818
}
4882-
ColumnTypeInfo info = STANDARD_TYPE_INFO.get(bqType);
4819+
ColumnTypeInfo info = BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(bqType);
48834820
if (info == null) {
48844821
LOG.warning("Unknown BigQuery type encountered: " + bqType.name() + ". Mapping to STRING.");
48854822
return new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null);

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcTypeMappings.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,64 @@ static Class<?> getJavaType(int javaSQLType) throws BigQueryJdbcSqlFeatureNotSup
158158
private static <K, V> SimpleEntry<K, V> entry(K key, V value) {
159159
return new SimpleEntry<>(key, value);
160160
}
161+
162+
static class ColumnTypeInfo {
163+
final int jdbcType;
164+
final String typeName;
165+
final Integer columnSize;
166+
final Integer decimalDigits;
167+
final Integer numPrecRadix;
168+
169+
ColumnTypeInfo(
170+
int jdbcType,
171+
String typeName,
172+
Integer columnSize,
173+
Integer decimalDigits,
174+
Integer numPrecRadix) {
175+
this.jdbcType = jdbcType;
176+
this.typeName = typeName;
177+
this.columnSize = columnSize;
178+
this.decimalDigits = decimalDigits;
179+
this.numPrecRadix = numPrecRadix;
180+
}
181+
}
182+
183+
static final Map<StandardSQLTypeName, ColumnTypeInfo> STANDARD_TYPE_INFO =
184+
ImmutableMap.<StandardSQLTypeName, ColumnTypeInfo>builder()
185+
.put(StandardSQLTypeName.INT64, new ColumnTypeInfo(Types.BIGINT, "INT64", 19, 0, 10))
186+
.put(StandardSQLTypeName.BOOL, new ColumnTypeInfo(Types.BOOLEAN, "BOOL", 1, null, null))
187+
.put(
188+
StandardSQLTypeName.FLOAT64,
189+
new ColumnTypeInfo(Types.DOUBLE, "FLOAT64", 15, null, 10))
190+
.put(StandardSQLTypeName.NUMERIC, new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 38, 9, 10))
191+
.put(
192+
StandardSQLTypeName.BIGNUMERIC,
193+
new ColumnTypeInfo(Types.NUMERIC, "BIGNUMERIC", 77, 38, 10))
194+
.put(
195+
StandardSQLTypeName.STRING,
196+
new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null))
197+
.put(
198+
StandardSQLTypeName.TIMESTAMP,
199+
new ColumnTypeInfo(Types.TIMESTAMP, "TIMESTAMP", 26, 6, null))
200+
.put(
201+
StandardSQLTypeName.DATETIME,
202+
new ColumnTypeInfo(Types.TIMESTAMP, "DATETIME", 26, 6, null))
203+
.put(StandardSQLTypeName.DATE, new ColumnTypeInfo(Types.DATE, "DATE", 10, 0, null))
204+
.put(StandardSQLTypeName.TIME, new ColumnTypeInfo(Types.TIME, "TIME", 15, 6, null))
205+
.put(
206+
StandardSQLTypeName.GEOGRAPHY,
207+
new ColumnTypeInfo(Types.OTHER, "GEOGRAPHY", null, null, null))
208+
.put(StandardSQLTypeName.JSON, new ColumnTypeInfo(Types.OTHER, "JSON", null, null, null))
209+
.put(
210+
StandardSQLTypeName.INTERVAL,
211+
new ColumnTypeInfo(Types.OTHER, "INTERVAL", null, null, null))
212+
.put(
213+
StandardSQLTypeName.RANGE, new ColumnTypeInfo(Types.OTHER, "RANGE", null, null, null))
214+
.put(
215+
StandardSQLTypeName.BYTES,
216+
new ColumnTypeInfo(Types.VARBINARY, "BYTES", null, null, null))
217+
.put(
218+
StandardSQLTypeName.STRUCT,
219+
new ColumnTypeInfo(Types.STRUCT, "STRUCT", null, null, null))
220+
.build();
161221
}

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryResultSetMetadata.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,32 @@ public String getColumnName(int column) {
134134

135135
@Override
136136
public int getPrecision(int column) {
137-
return (int) (getField(column).getPrecision() != null ? getField(column).getPrecision() : 0);
137+
Long precision = getField(column).getPrecision();
138+
if (precision != null) {
139+
return precision.intValue();
140+
}
141+
StandardSQLTypeName type = getStandardSQLTypeName(column);
142+
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
143+
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(type);
144+
if (typeInfo != null && typeInfo.columnSize != null) {
145+
return typeInfo.columnSize;
146+
}
147+
return 0;
138148
}
139149

140150
@Override
141151
public int getScale(int column) {
142-
return (int) (getField(column).getScale() != null ? getField(column).getScale() : 0);
152+
Long scale = getField(column).getScale();
153+
if (scale != null) {
154+
return scale.intValue();
155+
}
156+
StandardSQLTypeName type = getStandardSQLTypeName(column);
157+
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
158+
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(type);
159+
if (typeInfo != null && typeInfo.decimalDigits != null) {
160+
return typeInfo.decimalDigits;
161+
}
162+
return 0;
143163
}
144164

145165
@Override

0 commit comments

Comments
 (0)