Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.cloud.bigquery.TableDefinition;
import com.google.cloud.bigquery.TableId;
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import com.google.common.collect.ImmutableMap;
import java.io.BufferedReader;
import java.io.IOException;
Expand All @@ -61,7 +62,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
Expand All @@ -73,7 +74,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
Expand All @@ -92,7 +93,7 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
private static final String DATABASE_PRODUCT_NAME = "Google BigQuery";
private static final String DATABASE_PRODUCT_VERSION = "2.0";
private static final String DRIVER_NAME = "GoogleJDBCDriverForGoogleBigQuery";
private static final String DRIVER_DEFAULT_VERSION = "0.0.0";

private static final String SCHEMA_TERM = "Dataset";
private static final String CATALOG_TERM = "Project";
private static final String PROCEDURE_TERM = "Procedure";
Expand Down Expand Up @@ -143,18 +144,11 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
BigQueryConnection connection;
private final BigQuery bigquery;
private final int metadataFetchThreadCount;
private static final AtomicReference<String> parsedDriverVersion = new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMajorVersion =
new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMinorVersion =
new AtomicReference<>(null);

BigQueryDatabaseMetaData(BigQueryConnection connection) {
this.URL = connection.getConnectionUrl();
this.connection = connection;
this.bigquery = connection.getBigQuery();
this.metadataFetchThreadCount = connection.getMetadataFetchThreadCount();
loadDriverVersionProperties();
}

@Override
Expand Down Expand Up @@ -223,17 +217,17 @@ public String getDriverName() {

@Override
public String getDriverVersion() {
return parsedDriverVersion.get() != null ? parsedDriverVersion.get() : DRIVER_DEFAULT_VERSION;
return BigQueryJdbcVersionUtility.getDriverVersion();
}

@Override
public int getDriverMajorVersion() {
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
return BigQueryJdbcVersionUtility.getDriverMajorVersion();
}

@Override
public int getDriverMinorVersion() {
return parsedDriverMinorVersion.get() != null ? parsedDriverMinorVersion.get() : 0;
return BigQueryJdbcVersionUtility.getDriverMinorVersion();
}

@Override
Expand Down Expand Up @@ -5288,48 +5282,4 @@ String replaceSqlParameters(String sql, String... params) throws SQLException {
return String.format(sql, (Object[]) params);
}

private void loadDriverVersionProperties() {
if (parsedDriverVersion.get() != null) {
return;
}
Properties props = new Properties();
try (InputStream input =
getClass().getResourceAsStream("/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
if (input == null) {
String errorMessage =
"Could not find dependencies.properties. Driver version information is unavailable.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
props.load(input);
String versionString = props.getProperty("version.jdbc");
if (versionString == null || versionString.trim().isEmpty()) {
String errorMessage =
"The property version.jdbc not found or empty in dependencies.properties.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
parsedDriverVersion.compareAndSet(null, versionString.trim());
String[] parts = versionString.split("\\.");
if (parts.length < 2) {
return;
}
parsedDriverMajorVersion.compareAndSet(null, Integer.parseInt(parts[0]));
String minorPart = parts[1];
String numericMinor = minorPart.replaceAll("[^0-9].*", "");
if (!numericMinor.isEmpty()) {
parsedDriverMinorVersion.compareAndSet(null, Integer.parseInt(numericMinor));
}
} catch (IOException | NumberFormatException e) {
String errorMessage =
"Error reading dependencies.properties. Driver version information is"
+ " unavailable. Error: "
+ e.getMessage();
IllegalStateException ex = new IllegalStateException(errorMessage, e);
LOG.severe(errorMessage, ex);
throw ex;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.cloud.bigquery.exception.BigQueryJdbcException;
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import io.grpc.LoadBalancerRegistry;
import io.grpc.internal.PickFirstLoadBalancerProvider;
import java.io.IOException;
Expand Down Expand Up @@ -55,9 +56,6 @@ public class BigQueryDriver implements Driver {

private static final BigQueryJdbcCustomLogger LOG =
new BigQueryJdbcCustomLogger(BigQueryDriver.class.getName());
// TODO: update this when JDBC goes GA
private static final int JDBC_MAJOR_VERSION = 0;
private static final int JDBC_MINOR_VERSION = 1;
static BigQueryDriver registeredBigqueryJdbcDriver;

static {
Expand Down Expand Up @@ -232,13 +230,13 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
@Override
public int getMajorVersion() {
LOG.finest("++enter++");
return JDBC_MAJOR_VERSION;
return BigQueryJdbcVersionUtility.getDriverMajorVersion();
}

@Override
public int getMinorVersion() {
LOG.finest("++enter++");
return JDBC_MINOR_VERSION;
return BigQueryJdbcVersionUtility.getDriverMinorVersion();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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
*
* http://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.utils;

import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;

/** Utility class to load and parse the JDBC driver version from dependencies.properties. */
public final class BigQueryJdbcVersionUtility {

private static final BigQueryJdbcCustomLogger LOG =
new BigQueryJdbcCustomLogger(BigQueryJdbcVersionUtility.class.getName());

private static final AtomicReference<String> parsedDriverVersion = new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMajorVersion =
new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMinorVersion =
new AtomicReference<>(null);

private BigQueryJdbcVersionUtility() {
// Utility class, static methods only.
}

/**
* Gets the full driver version string.
*
* @return the driver version string, e.g., "1.0.0-SNAPSHOT"
*/
public static String getDriverVersion() {
ensureLoaded();
return parsedDriverVersion.get();
}

/**
* Gets the driver major version.
*
* @return the major version number
*/
public static int getDriverMajorVersion() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this and the minorVersion function with this new impl could throw IllegalStateException for BigQueryDriver.getMajorVersion() and getMinorVersion(), right?
I think we need to confirm JDBC spec if that's okay or should it throw SQL exception or should it default to something and not throw error at all

ensureLoaded();
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
}

/**
* Gets the driver minor version.
*
* @return the minor version number
*/
public static int getDriverMinorVersion() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

ensureLoaded();
return parsedDriverMinorVersion.get() != null ? parsedDriverMinorVersion.get() : 0;
}

private static void ensureLoaded() {
if (parsedDriverVersion.get() != null) {
return;
}
Properties props = new Properties();
try (InputStream input =
BigQueryJdbcVersionUtility.class.getResourceAsStream(
"/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
if (input == null) {
String errorMessage =
"Could not find dependencies.properties. Driver version information is unavailable.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
props.load(input);
String versionString = props.getProperty("version.jdbc");
if (versionString == null || versionString.trim().isEmpty()) {
String errorMessage =
"The property version.jdbc not found or empty in dependencies.properties.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
parsedDriverVersion.compareAndSet(null, versionString.trim());
String[] parts = versionString.split("\\.");
if (parts.length < 2) {
return;
}
parsedDriverMajorVersion.compareAndSet(null, Integer.parseInt(parts[0]));
String minorPart = parts[1];
String numericMinor = minorPart.replaceAll("[^0-9].*", "");
if (!numericMinor.isEmpty()) {
parsedDriverMinorVersion.compareAndSet(null, Integer.parseInt(numericMinor));
}
} catch (IOException | NumberFormatException e) {
String errorMessage =
"Error reading dependencies.properties. Driver version information is"
+ " unavailable. Error: "
+ e.getMessage();
IllegalStateException ex = new IllegalStateException(errorMessage, e);
LOG.severe(errorMessage, ex);
throw ex;
}
}
}
Comment on lines +17 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Concurrency Race Condition & Robustness Issue

There is a race condition in the lazy initialization logic of ensureLoaded(). If multiple threads call ensureLoaded() concurrently, one thread (Thread A) can set parsedDriverVersion but get context-switched before setting parsedDriverMajorVersion or parsedDriverMinorVersion. If another thread (Thread B) then calls getDriverMajorVersion(), it will see parsedDriverVersion.get() != null as true, skip loading, and return 0 (since parsedDriverMajorVersion is still null).

Additionally, if the version string has only one part (e.g., "1"), parts.length < 2 causes an early return, meaning the major version is never set and defaults to 0.

Solution

Since this is a utility class loading static classpath resources, using a static initializer block is the most standard, robust, and thread-safe approach. It completely eliminates the race condition, removes the need for AtomicReferences and ensureLoaded() overhead, and makes the fields private static final.

package com.google.cloud.bigquery.jdbc.utils;

import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/** Utility class to load and parse the JDBC driver version from dependencies.properties. */
public final class BigQueryJdbcVersionUtility {

  private static final BigQueryJdbcCustomLogger LOG =
      new BigQueryJdbcCustomLogger(BigQueryJdbcVersionUtility.class.getName());

  private static final String driverVersion;
  private static final int driverMajorVersion;
  private static final int driverMinorVersion;

  static {
    String version = "0.0.0";
    int major = 0;
    int minor = 0;
    try (InputStream input =
        BigQueryJdbcVersionUtility.class.getResourceAsStream(
            "/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
      if (input == null) {
        String errorMessage =
            "Could not find dependencies.properties. Driver version information is unavailable.";
        IllegalStateException ex = new IllegalStateException(errorMessage);
        LOG.severe(errorMessage, ex);
        throw ex;
      }
      Properties props = new Properties();
      props.load(input);
      String versionString = props.getProperty("version.jdbc");
      if (versionString == null || versionString.trim().isEmpty()) {
        String errorMessage =
            "The property version.jdbc not found or empty in dependencies.properties.";
        IllegalStateException ex = new IllegalStateException(errorMessage);
        LOG.severe(errorMessage, ex);
        throw ex;
      }
      version = versionString.trim();
      String[] parts = version.split("\\.");
      if (parts.length > 0) {
        major = Integer.parseInt(parts[0]);
      }
      if (parts.length > 1) {
        String minorPart = parts[1];
        String numericMinor = minorPart.replaceAll("[^0-9].*", "");
        if (!numericMinor.isEmpty()) {
          minor = Integer.parseInt(numericMinor);
        }
      }
    } catch (IOException | NumberFormatException e) {
      String errorMessage =
          "Error reading dependencies.properties. Driver version information is"
              + " unavailable. Error: "
              + e.getMessage();
      IllegalStateException ex = new IllegalStateException(errorMessage, e);
      LOG.severe(errorMessage, ex);
      throw ex;
    }
    driverVersion = version;
    driverMajorVersion = major;
    driverMinorVersion = minor;
  }

  private BigQueryJdbcVersionUtility() {
    // Utility class, static methods only.
  }

  /**
   * Gets the full driver version string.
   *
   * @return the driver version string, e.g., "1.0.0-SNAPSHOT"
   */
  public static String getDriverVersion() {
    return driverVersion;
  }

  /**
   * Gets the driver major version.
   *
   * @return the major version number
   */
  public static int getDriverMajorVersion() {
    return driverMajorVersion;
  }

  /**
   * Gets the driver minor version.
   *
   * @return the minor version number
   */
  public static int getDriverMinorVersion() {
    return driverMinorVersion;
  }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with gemini, we should address concurrency issue here

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static com.google.common.truth.Truth.assertThat;

import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import java.sql.Connection;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
Expand Down Expand Up @@ -76,12 +77,12 @@ public void testGetPropertyInfoReturnsValidProperties() {

@Test
public void testGetMajorVersionMatchesDriverMajorVersion() {
assertThat(bigQueryDriver.getMajorVersion()).isEqualTo(0);
assertThat(bigQueryDriver.getMajorVersion()).isEqualTo(BigQueryJdbcVersionUtility.getDriverMajorVersion());
}

@Test
public void testGetMinorVersionMatchesDriverMinorVersion() {
assertThat(bigQueryDriver.getMinorVersion()).isEqualTo(1);
assertThat(bigQueryDriver.getMinorVersion()).isEqualTo(BigQueryJdbcVersionUtility.getDriverMinorVersion());
}

@Test
Expand Down
Loading