From 23f66084d02a9ef4577aea4388b6f0d4491ba242 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 13 Oct 2026 03:48:14 +0000 Subject: [PATCH] perf(bigquery): [T1-5] eliminate BigDecimal/String.split allocations and precision loss in BigQueryResultImpl --- .../cloud/bigquery/BigQueryResultImpl.java | 82 +++++++++++++------ .../bigquery/BigQueryResultImplTest.java | 53 ++++++++++++ 2 files changed, 109 insertions(+), 26 deletions(-) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryResultImpl.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryResultImpl.java index c84724083f63..160f07fc9b94 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryResultImpl.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryResultImpl.java @@ -26,7 +26,6 @@ import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -120,15 +119,15 @@ private class BigQueryResultSet extends AbstractJdbcResultSet { private boolean wasNull = false; private class BigQueryArrayResult implements java.sql.Array { - List array; + List array; public BigQueryArrayResult(Object array) { + // Wrap Object[] views directly and retain existing List/FieldValueList instances + // without copying elements into a new ArrayList on every getArray call. if (array instanceof Object[]) { - this.array = new ArrayList<>(Arrays.asList((Object[]) array)); - } else if (array instanceof FieldValueList) { - this.array = new ArrayList<>((FieldValueList) array); + this.array = Arrays.asList((Object[]) array); } else { - this.array = (List) array; + this.array = (List) array; } } @@ -383,6 +382,22 @@ public String getString(int columnIndex) throws SQLException { } } + private int parseFieldValueInt(FieldValue fieldValue) { + String str = fieldValue.getStringValue(); + if (str.indexOf('.') == -1 && str.indexOf('e') == -1 && str.indexOf('E') == -1) { + return Integer.parseInt(str); + } + return fieldValue.getNumericValue().intValue(); + } + + private long parseFieldValueLong(FieldValue fieldValue) { + String str = fieldValue.getStringValue(); + if (str.indexOf('.') == -1 && str.indexOf('e') == -1 && str.indexOf('E') == -1) { + return Long.parseLong(str); + } + return fieldValue.getNumericValue().longValue(); + } + @Override public int getInt(String fieldName) throws SQLException { if (fieldName == null) { @@ -398,7 +413,7 @@ public int getInt(String fieldName) throws SQLException { return 0; } wasNull = false; - return fieldValue.getNumericValue().intValue(); + return parseFieldValueInt(fieldValue); } else { // Data received from Read API (Arrow) Object currentVal = getCurrentValueForReadApiData(fieldName); if (currentVal == null) { @@ -428,7 +443,8 @@ public int getInt(int columnIndex) throws SQLException { wasNull = true; return 0; } - return fieldValue.getNumericValue().intValue(); + wasNull = false; + return parseFieldValueInt(fieldValue); } else { // Data received from Read API (Arrow) return getInt(schemaFieldList.get(columnIndex).getName()); } @@ -448,7 +464,7 @@ public long getLong(String fieldName) throws SQLException { return 0L; } wasNull = false; - return fieldValue.getNumericValue().longValue(); + return parseFieldValueLong(fieldValue); } else { // Data received from Read API (Arrow) Object curVal = getCurrentValueForReadApiData(fieldName); if (curVal == null) { @@ -473,7 +489,7 @@ public long getLong(int columnIndex) throws SQLException { return 0L; } wasNull = false; - return fieldValue.getNumericValue().longValue(); + return parseFieldValueLong(fieldValue); } else { // Data received from Read API (Arrow) return getLong(schemaFieldList.get(columnIndex).getName()); } @@ -493,7 +509,7 @@ public double getDouble(String fieldName) throws SQLException { return 0.0d; } wasNull = false; - return fieldValue.getNumericValue().doubleValue(); + return fieldValue.getDoubleValue(); } else { // Data received from Read API (Arrow) Object curVal = getCurrentValueForReadApiData(fieldName); if (curVal == null) { @@ -501,7 +517,10 @@ public double getDouble(String fieldName) throws SQLException { return 0.0d; } wasNull = false; - return new BigDecimal(curVal.toString()).doubleValue(); + if (curVal instanceof Number) { + return ((Number) curVal).doubleValue(); + } + return Double.parseDouble(curVal.toString()); } } @@ -517,7 +536,7 @@ public double getDouble(int columnIndex) throws SQLException { return 0.0d; } wasNull = false; - return fieldValue.getNumericValue().doubleValue(); + return fieldValue.getDoubleValue(); } else { // Data received from Read API (Arrow) return getDouble(schemaFieldList.get(columnIndex).getName()); } @@ -537,7 +556,7 @@ public BigDecimal getBigDecimal(String fieldName) throws SQLException { return null; } wasNull = false; - return BigDecimal.valueOf(fieldValue.getNumericValue().doubleValue()); + return fieldValue.getNumericValue(); } else { // Data received from Read API (Arrow) Object curVal = getCurrentValueForReadApiData(fieldName); if (curVal == null) { @@ -545,7 +564,14 @@ public BigDecimal getBigDecimal(String fieldName) throws SQLException { return null; } wasNull = false; - return BigDecimal.valueOf(getDouble(fieldName)); + if (curVal instanceof BigDecimal) { + return (BigDecimal) curVal; + } else if (curVal instanceof Long || curVal instanceof Integer) { + return BigDecimal.valueOf(((Number) curVal).longValue()); + } else if (curVal instanceof Number) { + return BigDecimal.valueOf(((Number) curVal).doubleValue()); + } + return new BigDecimal(curVal.toString()); } } @@ -560,7 +586,7 @@ public BigDecimal getBigDecimal(int columnIndex) throws SQLException { return null; } wasNull = false; - return BigDecimal.valueOf(fieldValue.getNumericValue().doubleValue()); + return fieldValue.getNumericValue(); } else { // Data received from Read API (Arrow) return getBigDecimal(schemaFieldList.get(columnIndex).getName()); } @@ -751,19 +777,23 @@ public Time getTime(int columnIndex) throws SQLException { private Time getTimeFromFieldVal(FieldValue fieldValue) throws SQLException { // Time ranges from 00:00:00 to 23:59:59.99999. in BigQuery. Parsing it to java.sql.Time String strTime = fieldValue.getStringValue(); - String[] timeSplt = strTime.split(":"); - if (timeSplt.length != 3) { + int firstColon = strTime.indexOf(':'); + int secondColon = firstColon != -1 ? strTime.indexOf(':', firstColon + 1) : -1; + if (firstColon == -1 + || secondColon == -1 + || strTime.indexOf(':', secondColon + 1) != -1) { throw new SQLException("Can not parse the value " + strTime + " to java.sql.Time"); } - int hr = Integer.parseInt(timeSplt[0]); - int min = Integer.parseInt(timeSplt[1]); - int sec, nanoSec = 0; - if (timeSplt[2].contains(".")) { - String[] secSplt = timeSplt[2].split("\\."); - sec = Integer.parseInt(secSplt[0]); - nanoSec = Integer.parseInt(secSplt[1]); + int hr = Integer.parseInt(strTime.substring(0, firstColon)); + int min = Integer.parseInt(strTime.substring(firstColon + 1, secondColon)); + int sec; + int nanoSec = 0; + int dotIndex = strTime.indexOf('.', secondColon + 1); + if (dotIndex != -1) { + sec = Integer.parseInt(strTime.substring(secondColon + 1, dotIndex)); + nanoSec = Integer.parseInt(strTime.substring(dotIndex + 1)); } else { - sec = Integer.parseInt(timeSplt[2]); + sec = Integer.parseInt(strTime.substring(secondColon + 1)); } return Time.valueOf(LocalTime.of(hr, min, sec, nanoSec)); } diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryResultImplTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryResultImplTest.java index ab5543e1b6e9..b4c01eb9f5b0 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryResultImplTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryResultImplTest.java @@ -316,4 +316,57 @@ void testResultSetReadApiGetLongByColumnIndexPreservesInt64Value() assertThat(resultSet.wasNull()).isFalse(); assertThat(resultSet.next()).isFalse(); } + + @Test + void testResultSetNumericPrecisionWasNullAndFractionalTime() + throws InterruptedException, SQLException { + BigDecimal highPrecisionDecimal = + new BigDecimal("12345678901234567890123456789.123456789"); + BlockingQueue> fvlBuffer = new LinkedBlockingDeque<>(BUFFER_SIZE); + FieldValueList fvlRow = + FieldValueList.of( + ImmutableList.of( + FieldValue.of(Attribute.PRIMITIVE, null), + FieldValue.of(Attribute.PRIMITIVE, "42"), + FieldValue.of(Attribute.PRIMITIVE, highPrecisionDecimal.toPlainString()), + FieldValue.of(Attribute.PRIMITIVE, null), + FieldValue.of(Attribute.PRIMITIVE, null), + FieldValue.of(Attribute.PRIMITIVE, null), + FieldValue.of(Attribute.PRIMITIVE, "20:21:22.123456"), + FieldValue.of(Attribute.PRIMITIVE, null), + FieldValue.of(Attribute.REPEATED, null), + FieldValue.of(Attribute.REPEATED, null)), + FIELD_LIST_SCHEMA); + fvlBuffer.put(fvlRow); + fvlBuffer.put(new EndOfFieldValueList()); + + BigQueryResultImpl> fvlResult = + new BigQueryResultImpl<>(SCHEMA, 1, fvlBuffer, null); + ResultSet fvlRs = fvlResult.getResultSet(); + assertThat(fvlRs.next()).isTrue(); + // Read a null column first so wasNull becomes true, then verify getInt(int) resets wasNull + assertThat(fvlRs.getString(3)).isNull(); + assertThat(fvlRs.wasNull()).isTrue(); + assertThat(fvlRs.getInt(1)).isEqualTo(42); + assertThat(fvlRs.wasNull()).isFalse(); + assertThat(fvlRs.getBigDecimal("double")).isEqualTo(highPrecisionDecimal); + assertThat(fvlRs.getBigDecimal(2)).isEqualTo(highPrecisionDecimal); + assertThat(fvlRs.getTime("time")) + .isEqualTo(Time.valueOf(LocalTime.of(20, 21, 22, 123456))); + assertThat(fvlRs.next()).isFalse(); + + BlockingQueue rowBuffer = new LinkedBlockingDeque<>(BUFFER_SIZE); + Map rowValues = new HashMap<>(); + rowValues.put("double", highPrecisionDecimal); + rowBuffer.put(new BigQueryResultImpl.Row(rowValues)); + rowBuffer.put(new BigQueryResultImpl.Row(null, true)); + + BigQueryResultImpl rowResult = + new BigQueryResultImpl<>(SCHEMA, 1, rowBuffer, null); + ResultSet rowRs = rowResult.getResultSet(); + assertThat(rowRs.next()).isTrue(); + assertThat(rowRs.getBigDecimal("double")).isEqualTo(highPrecisionDecimal); + assertThat(rowRs.getBigDecimal(2)).isEqualTo(highPrecisionDecimal); + assertThat(rowRs.next()).isFalse(); + } }