From ad97128f8e2fd441f21fd6ba6dd6710e6789a12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 11 Aug 2026 16:59:22 +0200 Subject: [PATCH 1/2] perf(spanner-jdbc): cache JDBC metadata query strings Cache the SQL strings that are loaded from disk for the standard JDBC metadata queries. --- .../spanner/jdbc/JdbcDatabaseMetaData.java | 25 +++++++++++-------- .../jdbc/JdbcDatabaseMetaDataTest.java | 9 +++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java index 7168e2f9204b..f4418b09148f 100644 --- a/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java +++ b/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java @@ -26,10 +26,12 @@ import com.google.cloud.spanner.Type.StructField; import com.google.cloud.spanner.connection.Connection.InternalMetadataQuery; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; @@ -40,6 +42,8 @@ import java.util.Collections; import java.util.Properties; import java.util.Scanner; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** {@link DatabaseMetaData} implementation for Cloud Spanner */ class JdbcDatabaseMetaData extends AbstractJdbcWrapper implements DatabaseMetaData { @@ -50,18 +54,19 @@ class JdbcDatabaseMetaData extends AbstractJdbcWrapper implements DatabaseMetaDa private static final String PRODUCT_NAME = "Google Cloud Spanner"; private static final String POSTGRESQL_PRODUCT_NAME = PRODUCT_NAME + " PostgreSQL"; + private static final ConcurrentMap SQL_CACHE = new ConcurrentHashMap<>(); + @VisibleForTesting static String readSqlFromFile(String filename, Dialect dialect) { - InputStream in; - switch (dialect) { - case POSTGRESQL: - in = JdbcDatabaseMetaData.class.getResourceAsStream("postgresql/" + filename); - break; - case GOOGLE_STANDARD_SQL: - default: - in = JdbcDatabaseMetaData.class.getResourceAsStream(filename); - } - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + return SQL_CACHE.computeIfAbsent( + filename + "/" + dialect, (key) -> loadSqlFromFile(filename, dialect)); + } + + private static String loadSqlFromFile(String filename, Dialect dialect) { + String resourcePath = dialect == Dialect.POSTGRESQL ? "postgresql/" + filename : filename; + InputStream in = JdbcDatabaseMetaData.class.getResourceAsStream(resourcePath); + Preconditions.checkNotNull(in, "Resource not found: " + resourcePath); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); StringBuilder builder = new StringBuilder(); try (Scanner scanner = new Scanner(reader)) { while (scanner.hasNextLine()) { diff --git a/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java b/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java index fc3168ab4f0a..f6e4bd7948d4 100644 --- a/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java +++ b/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -574,4 +575,12 @@ public void testGetUserName() throws SQLException, IOException { DatabaseMetaData meta = new JdbcDatabaseMetaData(connection); assertEquals("test@test-project.iam.gserviceaccount.com", meta.getUserName()); } + + @Test + public void testReadSqlFromFileCaching() { + String sql1 = JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql", dialect); + String sql2 = JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql", dialect); + assertNotNull(sql1); + assertSame(sql1, sql2); + } } From 14bac61a8a0f63d25bd71b03f18a9a492fe8f920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 13 Aug 2026 18:05:58 +0200 Subject: [PATCH 2/2] chore(spanner-jdbc): address review comments --- .../spanner/jdbc/JdbcDatabaseMetaData.java | 29 ++++++++++++++----- .../jdbc/JdbcDatabaseMetaDataTest.java | 12 +++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java index f4418b09148f..c7f30fcec1eb 100644 --- a/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java +++ b/java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java @@ -20,15 +20,17 @@ import com.google.auth.ServiceAccountSigner; import com.google.auth.oauth2.UserCredentials; import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; import com.google.cloud.spanner.connection.Connection.InternalMetadataQuery; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; -import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -57,22 +59,33 @@ class JdbcDatabaseMetaData extends AbstractJdbcWrapper implements DatabaseMetaDa private static final ConcurrentMap SQL_CACHE = new ConcurrentHashMap<>(); @VisibleForTesting - static String readSqlFromFile(String filename, Dialect dialect) { - return SQL_CACHE.computeIfAbsent( - filename + "/" + dialect, (key) -> loadSqlFromFile(filename, dialect)); + static String readSqlFromFile(String filename, Dialect dialect) throws SQLException { + try { + return SQL_CACHE.computeIfAbsent( + filename + "/" + dialect, (key) -> loadSqlFromFile(filename, dialect)); + } catch (SpannerException e) { + throw JdbcSqlExceptionFactory.of(e); + } } private static String loadSqlFromFile(String filename, Dialect dialect) { String resourcePath = dialect == Dialect.POSTGRESQL ? "postgresql/" + filename : filename; InputStream in = JdbcDatabaseMetaData.class.getResourceAsStream(resourcePath); - Preconditions.checkNotNull(in, "Resource not found: " + resourcePath); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + if (in == null) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.NOT_FOUND, "Resource not found: " + resourcePath); + } StringBuilder builder = new StringBuilder(); - try (Scanner scanner = new Scanner(reader)) { + try (InputStream input = in; + InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8); + Scanner scanner = new Scanner(reader)) { while (scanner.hasNextLine()) { String line = scanner.nextLine(); builder.append(line).append("\n"); } + } catch (IOException e) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INTERNAL, "Could not read SQL file " + resourcePath, e); } return builder.toString(); } diff --git a/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java b/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java index f6e4bd7948d4..935643930701 100644 --- a/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java +++ b/java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -577,10 +578,19 @@ public void testGetUserName() throws SQLException, IOException { } @Test - public void testReadSqlFromFileCaching() { + public void testReadSqlFromFileCaching() throws SQLException { String sql1 = JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql", dialect); String sql2 = JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql", dialect); assertNotNull(sql1); assertSame(sql1, sql2); } + + @Test + public void testReadSqlFromFileNotFound() { + SQLException exception = + assertThrows( + SQLException.class, + () -> JdbcDatabaseMetaData.readSqlFromFile("NonExistent.sql", dialect)); + assertTrue(exception.getMessage().contains("Resource not found")); + } }