Skip to content

fix(bigquery-jdbc): add proper version to BigQueryDriver#13294

Open
logachev wants to merge 1 commit into
mainfrom
kirl/fix_version
Open

fix(bigquery-jdbc): add proper version to BigQueryDriver#13294
logachev wants to merge 1 commit into
mainfrom
kirl/fix_version

Conversation

@logachev
Copy link
Copy Markdown
Contributor

Update BigQueryDriver getMinorVersion/getMajorVersion to return proper value

@logachev logachev requested review from a team as code owners May 28, 2026 20:25
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the JDBC driver version loading and parsing logic by moving it from BigQueryDatabaseMetaData into a new utility class, BigQueryJdbcVersionUtility, which is then used by both BigQueryDatabaseMetaData and BigQueryDriver. A critical review comment points out a concurrency race condition in the lazy initialization of the utility class, which could lead to threads reading uninitialized major or minor versions. Additionally, the comment highlights a bug where single-part version strings would prevent the major version from being set. The reviewer suggests resolving these issues by using a thread-safe static initializer block to load the properties.

Comment on lines +17 to +116
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() {
ensureLoaded();
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
}

/**
* Gets the driver minor version.
*
* @return the minor version number
*/
public static int getDriverMinorVersion() {
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;
}
}
}
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

*
* @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

*
* @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

Comment on lines +17 to +116
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() {
ensureLoaded();
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
}

/**
* Gets the driver minor version.
*
* @return the minor version number
*/
public static int getDriverMinorVersion() {
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;
}
}
}
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

@keshavdandeva
Copy link
Copy Markdown
Contributor

Can we also update the README in this PR for the maven link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants