From d7e2a060f0ee41165edd5c3d30bfb4e0afd24a10 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Mon, 7 Sep 2026 15:01:01 +0000 Subject: [PATCH 1/2] feat(bigquery-jdbc): add `EnableTimestampPicos` connection property and its plumbing --- java-bigquery-jdbc/docs/USER_GUIDE.md | 6 +++++ .../bigquery/jdbc/BigQueryConnection.java | 7 ++++++ .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 10 +++++++++ .../cloud/bigquery/jdbc/BigQuerySettings.java | 22 +++++++++++++++++++ .../bigquery/jdbc/BigQueryStatement.java | 10 +++++++++ .../cloud/bigquery/jdbc/DataSource.java | 22 +++++++++++++++++++ .../bigquery/jdbc/BigQueryConnectionTest.java | 15 +++++++++++++ .../jdbc/BigQueryJdbcUrlUtilityTest.java | 19 ++++++++++++++++ .../bigquery/jdbc/BigQueryStatementTest.java | 20 +++++++++++++++++ 9 files changed, 131 insertions(+) diff --git a/java-bigquery-jdbc/docs/USER_GUIDE.md b/java-bigquery-jdbc/docs/USER_GUIDE.md index 0f38e0fe252e..ecfa60b113bb 100644 --- a/java-bigquery-jdbc/docs/USER_GUIDE.md +++ b/java-bigquery-jdbc/docs/USER_GUIDE.md @@ -207,6 +207,12 @@ String url = "jdbc:bigquery://https://bigquery.googleapis.com:443" | :--- | :---: | :--- | | `EnableSession` | `false` | Enables multi-statement session creation and transaction support (`BEGIN`, `COMMIT`, `ROLLBACK`). | +### Data Types & Extended Precision Properties + +| Property Name | Default Value | Description | +| :--- | :---: | :--- | +| `EnableTimestampPicos` | `false` | Enables 12-digit picosecond precision for `TIMESTAMP(12)` data types. When enabled, `TIMESTAMP(12)` columns are retrieved with 12 fractional digits formatted in UTC (`YYYY-MM-DD HH:MM:SS.ffffffffffff`) via `getString()` and `getObject()`, typed as `Types.VARCHAR` (`12`) with type name `"TIMESTAMP_PICOSECONDS"`. Picosecond precision is incompatible with Legacy SQL (`QueryDialect=BIG_QUERY`). | + ### High-Throughput Storage & Write API Properties | Property Name | Default Value | Description | diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java index d394b1ba7a7e..3d2ae4e9e02e 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java @@ -104,6 +104,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { BigQueryJdbcUrlUtility.KMS_KEY_NAME_PROPERTY_NAME, BigQueryJdbcUrlUtility.QUERY_PROPERTIES_NAME, BigQueryJdbcUrlUtility.ENABLE_SESSION_PROPERTY_NAME, + BigQueryJdbcUrlUtility.ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME, BigQueryJdbcUrlUtility.LOG_LEVEL_PROPERTY_NAME, BigQueryJdbcUrlUtility.LOG_PATH_PROPERTY_NAME, BigQueryJdbcUrlUtility.OAUTH_TYPE_PROPERTY_NAME, @@ -186,6 +187,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { int highThroughputMinTableSize; int highThroughputActivationRatio; boolean enableSession; + boolean enableTimestampPicos; boolean enableProjectDiscovery; private List discoveredProjectsCache; boolean unsupportedHTAPIFallback; @@ -357,6 +359,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { this.sslTrustStoreProvider, this.connectionClassName); this.enableSession = ds.getEnableSession(); + this.enableTimestampPicos = ds.getEnableTimestampPicos(); this.unsupportedHTAPIFallback = ds.getUnsupportedHTAPIFallback(); this.maxResults = ds.getMaxResults(); Map queryPropertiesMap = ds.getQueryProperties(); @@ -719,6 +722,10 @@ boolean isSessionEnabled() { return this.enableSession; } + boolean isEnableTimestampPicos() { + return this.enableTimestampPicos; + } + boolean isUnsupportedHTAPIFallback() { return this.unsupportedHTAPIFallback; } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 14f74f8046a3..ea022c7c9e51 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -123,6 +123,8 @@ protected boolean removeEldestEntry(Map.Entry> eldes static final String LOG_LEVEL_ENV_VAR = "BIGQUERY_JDBC_LOG_LEVEL"; static final String LOG_PATH_ENV_VAR = "BIGQUERY_JDBC_LOG_PATH"; static final String ENABLE_SESSION_PROPERTY_NAME = "EnableSession"; + static final String ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME = "EnableTimestampPicos"; + static final boolean DEFAULT_ENABLE_TIMESTAMP_PICOS_VALUE = false; static final String DEFAULT_LOG_PATH = ""; static final String USE_QUERY_CACHE_PROPERTY_NAME = "UseQueryCache"; static final boolean DEFAULT_USE_QUERY_CACHE = true; @@ -446,6 +448,14 @@ protected boolean removeEldestEntry(Map.Entry> eldes + " transactions. Disabled by default.") .setDefaultValue(String.valueOf(DEFAULT_ENABLE_SESSION_VALUE)) .build(), + BigQueryConnectionProperty.newBuilder() + .setName(ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME) + .setDescription( + "Enables picosecond precision for TIMESTAMP data types. When enabled," + + " TIMESTAMP(12) columns are retrieved with 12-digit picosecond" + + " precision formatted in UTC. Disabled by default.") + .setDefaultValue(String.valueOf(DEFAULT_ENABLE_TIMESTAMP_PICOS_VALUE)) + .build(), BigQueryConnectionProperty.newBuilder() .setName(LOG_LEVEL_PROPERTY_NAME) .setDescription( diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQuerySettings.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQuerySettings.java index d94b9b706f35..bc1b932022f5 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQuerySettings.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQuerySettings.java @@ -43,6 +43,7 @@ class BigQuerySettings { private final boolean unsupportedHTAPIFallback; private final boolean enableSession; + private final boolean enableTimestampPicos; private final ConnectionProperty sessionInfoConnectionProperty; @@ -120,6 +121,7 @@ private BigQuerySettings(Builder builder) { this.maxResultPerPage = builder.maxResultPerPage; this.defaultDataset = builder.defaultDataset; this.enableSession = builder.enableSession; + this.enableTimestampPicos = builder.enableTimestampPicos; this.unsupportedHTAPIFallback = builder.unsupportedHTAPIFallback; this.sessionInfoConnectionProperty = builder.sessionInfoConnectionProperty; this.useWriteAPI = builder.useWriteAPI; @@ -171,6 +173,15 @@ boolean isEnableSession() { return enableSession; } + /** + * Returns whether picosecond precision is enabled for TIMESTAMP data types. + * + * @return true if picosecond precision is enabled, false otherwise. + */ + boolean isEnableTimestampPicos() { + return enableTimestampPicos; + } + /** * When the connector uses fetch workflows not supported on the High-Throughput API, this option * specifies whether the connector falls back to the REST API or returns an error. By default it @@ -476,6 +487,7 @@ static final class Builder { private int highThroughputMinTableSize; private int highThroughputActivationRatio; private boolean enableSession; + private boolean enableTimestampPicos; private boolean unsupportedHTAPIFallback; private ConnectionProperty sessionInfoConnectionProperty; private boolean useQueryCache; @@ -516,6 +528,7 @@ private Builder(BigQuerySettings querySettings) { this.highThroughputMinTableSize = querySettings.getHighThroughputMinTableSize(); this.highThroughputActivationRatio = querySettings.getHighThroughputActivationRatio(); this.enableSession = querySettings.isEnableSession(); + this.enableTimestampPicos = querySettings.isEnableTimestampPicos(); this.unsupportedHTAPIFallback = querySettings.isUnsupportedHTAPIFallback(); this.sessionInfoConnectionProperty = querySettings.getSessionInfoConnectionProperty(); this.useQueryCache = querySettings.getUseQueryCache(); @@ -608,6 +621,15 @@ Builder setEnableSession(boolean enableSession) { return this; } + /** + * Setting true enables 12-digit picosecond precision for TIMESTAMP(12) columns. Disabled by + * default. + */ + Builder setEnableTimestampPicos(boolean enableTimestampPicos) { + this.enableTimestampPicos = enableTimestampPicos; + return this; + } + /** * When the connector uses fetch workflows not supported on the High-Throughput API, this option * specifies whether the connector falls back to the REST API or returns an error. By default it diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index 3fd724c820d9..740e575b3171 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -216,6 +216,7 @@ private BigQuerySettings generateBigQuerySettings() { querySettings.setUseWriteAPI(this.connection.isEnableWriteAPI()); querySettings.setWriteAPIActivationRowCount(this.connection.getWriteAPIActivationRowCount()); querySettings.setWriteAPIAppendRowCount(this.connection.getWriteAPIAppendRowCount()); + querySettings.setEnableTimestampPicos(this.connection.isEnableTimestampPicos()); return querySettings.build(); } @@ -661,6 +662,11 @@ void runQuery(String query, QueryJobConfiguration jobConfiguration) } catch (InterruptedException ex) { throw new BigQueryJdbcRuntimeException("Interrupted during runQuery", ex); } catch (BigQueryException ex) { + if (isEnableTimestampPicos() && getUseLegacySql()) { + throw new BigQueryJdbcException( + "Picosecond data is incompatible with Legacy SQL. To query your Picosecond data, please set QueryDialect to SQL and restructure your query as a Standard SQL query.", + ex); + } if (ex.getMessage().contains("Syntax error")) { throw new BigQueryJdbcSqlSyntaxErrorException("BigQueryException during runQuery", ex); } @@ -1534,6 +1540,10 @@ private boolean getUseLegacySql() { QueryDialectType.valueOf(this.querySettings.getQueryDialect())); } + boolean isEnableTimestampPicos() { + return this.querySettings.isEnableTimestampPicos(); + } + private void checkIfDatasetExistElseCreate(String datasetName) { Dataset dataset = bigQuery.getDataset(DatasetId.of(datasetName)); if (dataset == null) { diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index 2c07fc483bac..033cc54e2256 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -58,6 +58,7 @@ public class DataSource implements javax.sql.DataSource { private Map queryProperties; private String logLevel; private Boolean enableSession; + private Boolean enableTimestampPicos; private String logPath; private String gcpTelemetryProjectId; private String gcpTelemetryCredentials; @@ -180,6 +181,12 @@ public class DataSource implements javax.sql.DataSource { ds.setEnableSession( BigQueryJdbcUrlUtility.convertIntToBoolean( val, BigQueryJdbcUrlUtility.ENABLE_SESSION_PROPERTY_NAME))) + .put( + BigQueryJdbcUrlUtility.ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME, + (ds, val) -> + ds.setEnableTimestampPicos( + BigQueryJdbcUrlUtility.convertIntToBoolean( + val, BigQueryJdbcUrlUtility.ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME))) .put(BigQueryJdbcUrlUtility.LOG_LEVEL_PROPERTY_NAME, DataSource::setLogLevel) .put(BigQueryJdbcUrlUtility.LOG_PATH_PROPERTY_NAME, DataSource::setLogPath) .put( @@ -490,6 +497,11 @@ Properties createProperties() { connectionProperties.setProperty( BigQueryJdbcUrlUtility.ENABLE_SESSION_PROPERTY_NAME, String.valueOf(this.enableSession)); } + if (this.enableTimestampPicos != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME, + String.valueOf(this.enableTimestampPicos)); + } if (this.logLevel != null) { connectionProperties.setProperty( BigQueryJdbcUrlUtility.LOG_LEVEL_PROPERTY_NAME, this.logLevel); @@ -935,6 +947,16 @@ public void setEnableSession(Boolean enableSession) { this.enableSession = enableSession; } + public Boolean getEnableTimestampPicos() { + return enableTimestampPicos != null + ? enableTimestampPicos + : BigQueryJdbcUrlUtility.DEFAULT_ENABLE_TIMESTAMP_PICOS_VALUE; + } + + public void setEnableTimestampPicos(Boolean enableTimestampPicos) { + this.enableTimestampPicos = enableTimestampPicos; + } + public String getLogLevel() { return logLevel; } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index 7b01f9ac760e..3758fed968ff 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -819,4 +819,19 @@ public void testUserSuppliedSessionId() throws Exception { "user_supplied_session_999", connection.getSessionInfoConnectionProperty().getValue()); } } + + @Test + public void testEnableTimestampPicosDefault() throws Exception { + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL)) { + assertFalse(connection.isEnableTimestampPicos()); + } + } + + @Test + public void testEnableTimestampPicosConfigured() throws Exception { + String url = BASE_URL + "EnableTimestampPicos=1;"; + try (BigQueryConnection connection = new BigQueryConnection(url)) { + assertTrue(connection.isEnableTimestampPicos()); + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 36a8f7fdde48..d580f95bbaed 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -348,4 +348,23 @@ public void testParseDefaultDataset_invalidFormats() { BigQueryJdbcRuntimeException.class, () -> BigQueryJdbcUrlUtility.parseDefaultDataset(" : ")); } + + @Test + public void testParseEnableTimestampPicos() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "EnableTimestampPicos=1"; + + String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "EnableTimestampPicos"); + assertThat(result).isEqualTo("1"); + + String url2 = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "EnableTimestampPicos=0"; + + String result2 = BigQueryJdbcUrlUtility.parseUriProperty(url2, "EnableTimestampPicos"); + assertThat(result2).isEqualTo("0"); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java index 00b401440a2a..241607cd1d2c 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -1159,4 +1160,23 @@ public void testNullStatementTypeInTableResultFallsBackToGetJobWithoutDryRun() t verify(bigquery, Mockito.times(1)).getJob(eq(this.jobId)); verify(bigquery, Mockito.never()).create(any(JobInfo.class)); } + + @Test + public void testLegacySqlWithEnableTimestampPicosTranslatesBackendException() throws Exception { + doReturn(BigQueryStatement.QueryDialectType.BIG_QUERY.name()) + .when(bigQueryConnection) + .getQueryDialect(); + doReturn(true).when(bigQueryConnection).isEnableTimestampPicos(); + BigQueryException backendException = new BigQueryException(400, "Backend error"); + Mockito.doThrow(backendException) + .when(bigquery) + .queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any()); + + BigQueryStatement statement = new BigQueryStatement(bigQueryConnection); + + BigQueryJdbcException ex = + assertThrows(BigQueryJdbcException.class, () -> statement.executeQuery("SELECT 1")); + assertThat(ex.getMessage()).contains("Picosecond data is incompatible with Legacy SQL"); + assertThat(ex.getCause()).isSameInstanceAs(backendException); + } } From 03054c75e9742c6c4bac282e7fcc3c3c00780e78 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Mon, 7 Sep 2026 15:25:40 +0000 Subject: [PATCH 2/2] remove premature exception check --- .../cloud/bigquery/jdbc/BigQueryStatement.java | 5 ----- .../bigquery/jdbc/BigQueryStatementTest.java | 17 ++--------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index 740e575b3171..c0e5820289bc 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -662,11 +662,6 @@ void runQuery(String query, QueryJobConfiguration jobConfiguration) } catch (InterruptedException ex) { throw new BigQueryJdbcRuntimeException("Interrupted during runQuery", ex); } catch (BigQueryException ex) { - if (isEnableTimestampPicos() && getUseLegacySql()) { - throw new BigQueryJdbcException( - "Picosecond data is incompatible with Legacy SQL. To query your Picosecond data, please set QueryDialect to SQL and restructure your query as a Standard SQL query.", - ex); - } if (ex.getMessage().contains("Syntax error")) { throw new BigQueryJdbcSqlSyntaxErrorException("BigQueryException during runQuery", ex); } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java index 241607cd1d2c..d72d38580498 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -1162,21 +1161,9 @@ public void testNullStatementTypeInTableResultFallsBackToGetJobWithoutDryRun() t } @Test - public void testLegacySqlWithEnableTimestampPicosTranslatesBackendException() throws Exception { - doReturn(BigQueryStatement.QueryDialectType.BIG_QUERY.name()) - .when(bigQueryConnection) - .getQueryDialect(); + public void testEnableTimestampPicosPropagation() { doReturn(true).when(bigQueryConnection).isEnableTimestampPicos(); - BigQueryException backendException = new BigQueryException(400, "Backend error"); - Mockito.doThrow(backendException) - .when(bigquery) - .queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any()); - BigQueryStatement statement = new BigQueryStatement(bigQueryConnection); - - BigQueryJdbcException ex = - assertThrows(BigQueryJdbcException.class, () -> statement.executeQuery("SELECT 1")); - assertThat(ex.getMessage()).contains("Picosecond data is incompatible with Legacy SQL"); - assertThat(ex.getCause()).isSameInstanceAs(backendException); + assertTrue(statement.isEnableTimestampPicos()); } }