Skip to content

Commit c9738ea

Browse files
perf(bigquery-jdbc): optimize getExportedKeys performance using hybrid metadata lookup (#13734)
b/534326814 This PR optimizes `DatabaseMetaData.getExportedKeys()` by restoring the highly performant `INFORMATION_SCHEMA` SQL lookup path, correcting a performance regression introduced when the method was migrated to sequential REST API lookups. To ensure complete contract safety across both standard native tables and external REST catalog tables (PCNT tables), we implement a hybrid routing strategy: 1. **PCNT Early Return (Fast Path)**: If `schema` contains a dot (`.`), we bypass SQL execution entirely and return a structured empty `ResultSet` instantly. 2. **Wildcard Fallback (REST API Path)**: If either `catalog` or `schema` arguments are `null` (representing wildcard metadata searches), the driver falls back to retrieving constraints concurrently via the Java Client REST API. 3. **Standard Introspection (SQL Path)**: For targeted native table queries, the driver executes the optimized `DatabaseMetaData_GetExportedKeys.sql` query template.
1 parent 1bc16bb commit c9738ea

4 files changed

Lines changed: 179 additions & 34 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,17 @@
5252
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
5353
import com.google.cloud.bigquery.jdbc.BigQueryJdbcTypeMappings.ColumnTypeInfo;
5454
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
55+
import java.io.ByteArrayOutputStream;
56+
import java.io.IOException;
57+
import java.io.InputStream;
58+
import java.nio.charset.StandardCharsets;
5559
import java.sql.Connection;
5660
import java.sql.DatabaseMetaData;
61+
import java.sql.PreparedStatement;
5762
import java.sql.ResultSet;
5863
import java.sql.RowIdLifetime;
5964
import java.sql.SQLException;
65+
import java.sql.Statement;
6066
import java.sql.Types;
6167
import java.util.ArrayList;
6268
import java.util.Arrays;
@@ -94,6 +100,8 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
94100
private static final String PROCEDURE_TERM = "Procedure";
95101
private static final int DEFAULT_PAGE_SIZE = 500;
96102
private static final int DEFAULT_QUEUE_CAPACITY = 5000;
103+
private static final String GET_EXPORTED_KEYS_SQL = "DatabaseMetaData_GetExportedKeys.sql";
104+
private static String exportedKeysSqlContent;
97105
// Declared package-private for testing.
98106
static final String GOOGLE_SQL_QUOTED_IDENTIFIER = "`";
99107
// Does not include SQL:2003 Keywords as per JDBC spec.
@@ -2559,40 +2567,65 @@ public ResultSet getExportedKeys(String catalog, String schema, String table)
25592567
final Schema resultSchema = defineForeignKeyResultSetSchema();
25602568
final FieldList resultSchemaFields = resultSchema.getFields();
25612569

2562-
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
2563-
List<DatasetId> targetDatasets = getTargetDatasets(catalog, null);
2570+
// Early return for PCNT catalog schemas (containing '.') as they do not support table
2571+
// constraints.
2572+
if (schema != null && schema.contains(".")) {
2573+
final BlockingQueue<BigQueryFieldValueListWrapper> queue = new LinkedBlockingQueue<>(1);
2574+
signalEndOfData(queue, resultSchemaFields);
2575+
return BigQueryJsonResultSet.of(resultSchema, 0, queue, null);
2576+
}
25642577

2565-
boolean ignoreAccessErrors = (catalog == null);
2566-
processTargetTablesConcurrently(
2567-
targetDatasets,
2568-
null,
2569-
collectedResults,
2570-
resultSchemaFields,
2571-
ignoreAccessErrors,
2572-
(bqTable, results, fields) -> {
2573-
TableConstraints constraints = bqTable.getTableConstraints();
2574-
if (constraints == null || constraints.getForeignKeys() == null) {
2575-
return;
2576-
}
2577-
for (ForeignKey fk : constraints.getForeignKeys()) {
2578-
TableId pkTableId = fk.getReferencedTable();
2579-
if (pkTableId == null
2580-
|| !equalsOrNullMatchesAll(catalog, pkTableId.getProject())
2581-
|| !equalsOrNullMatchesAll(schema, pkTableId.getDataset())
2582-
|| !table.equals(pkTableId.getTable())) {
2583-
continue;
2578+
// Fallback Path: If catalog or schema is null, fall back to REST API metadata scan.
2579+
if (catalog == null || schema == null) {
2580+
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
2581+
List<DatasetId> targetDatasets = getTargetDatasets(catalog, schema);
2582+
2583+
boolean ignoreAccessErrors = (catalog == null);
2584+
processTargetTablesConcurrently(
2585+
targetDatasets,
2586+
null,
2587+
collectedResults,
2588+
resultSchemaFields,
2589+
ignoreAccessErrors,
2590+
(bqTable, results, fields) -> {
2591+
TableConstraints constraints = bqTable.getTableConstraints();
2592+
if (constraints == null || constraints.getForeignKeys() == null) {
2593+
return;
25842594
}
2585-
processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields);
2586-
}
2587-
});
2595+
for (ForeignKey fk : constraints.getForeignKeys()) {
2596+
TableId pkTableId = fk.getReferencedTable();
2597+
if (pkTableId == null
2598+
|| !equalsOrNullMatchesAll(catalog, pkTableId.getProject())
2599+
|| !equalsOrNullMatchesAll(schema, pkTableId.getDataset())
2600+
|| !table.equals(pkTableId.getTable())) {
2601+
continue;
2602+
}
2603+
processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields);
2604+
}
2605+
});
25882606

2589-
Comparator<FieldValueList> comparator = defineFkTableSortComparator(resultSchemaFields);
2590-
sortResults(collectedResults, comparator, "getExportedKeys", LOG);
2607+
Comparator<FieldValueList> comparator = defineFkTableSortComparator(resultSchemaFields);
2608+
sortResults(collectedResults, comparator, "getExportedKeys", LOG);
25912609

2592-
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
2593-
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2594-
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
2595-
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
2610+
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
2611+
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2612+
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
2613+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
2614+
}
2615+
2616+
String sql = getExportedKeysSqlContent();
2617+
String formattedSql = replaceSqlParameters(sql, catalog, schema, table);
2618+
PreparedStatement stmt = this.connection.prepareStatement(formattedSql);
2619+
if (stmt == null) {
2620+
throw new BigQueryJdbcException("Failed to prepare statement for getExportedKeys");
2621+
}
2622+
try {
2623+
stmt.closeOnCompletion();
2624+
return stmt.executeQuery();
2625+
} catch (SQLException e) {
2626+
closeStatementIgnoreException(stmt);
2627+
throw new BigQueryJdbcException("Error executing getExportedKeys", e);
2628+
}
25962629
}
25972630

25982631
@Override
@@ -5355,4 +5388,43 @@ private Comparator<FieldValueList> defineFkTableSortComparator(FieldList resultS
53555388
(FieldValueList fvl) -> getLongValueOrNull(fvl, KEY_SEQ_IDX),
53565389
Comparator.nullsFirst(Long::compareTo));
53575390
}
5391+
5392+
private static synchronized String getExportedKeysSqlContent() {
5393+
if (exportedKeysSqlContent == null) {
5394+
exportedKeysSqlContent = readSqlFromFile(GET_EXPORTED_KEYS_SQL);
5395+
}
5396+
return exportedKeysSqlContent;
5397+
}
5398+
5399+
static String readSqlFromFile(String filename) {
5400+
try (InputStream in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename)) {
5401+
if (in == null) {
5402+
throw new IllegalArgumentException("SQL file not found: " + filename);
5403+
}
5404+
ByteArrayOutputStream result = new ByteArrayOutputStream();
5405+
byte[] buffer = new byte[1024];
5406+
int length;
5407+
while ((length = in.read(buffer)) != -1) {
5408+
result.write(buffer, 0, length);
5409+
}
5410+
return result.toString(StandardCharsets.UTF_8.name());
5411+
} catch (IOException e) {
5412+
throw new RuntimeException("Failed to read SQL file: " + filename, e);
5413+
}
5414+
}
5415+
5416+
String replaceSqlParameters(String sql, String... params) throws SQLException {
5417+
return String.format(sql, (Object[]) params);
5418+
}
5419+
5420+
private void closeStatementIgnoreException(Statement stmt) {
5421+
if (stmt == null) {
5422+
return;
5423+
}
5424+
try {
5425+
stmt.close();
5426+
} catch (SQLException e) {
5427+
// ignore
5428+
}
5429+
}
53585430
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
SELECT
18+
pk.table_catalog AS PKTABLE_CAT,
19+
pk.table_schema AS PKTABLE_SCHEM,
20+
pk.table_name AS PKTABLE_NAME,
21+
pk.column_name AS PKCOLUMN_NAME,
22+
fk.table_catalog AS FKTABLE_CAT,
23+
fk.table_schema AS FKTABLE_SCHEM,
24+
fk.table_name AS FKTABLE_NAME,
25+
fk.column_name AS FKCOLUMN_NAME,
26+
fk.ordinal_position AS KEY_SEQ,
27+
1 AS UPDATE_RULE,
28+
1 AS DELETE_RULE,
29+
REGEXP_EXTRACT(fk.constraint_name, r'[^.]+$') AS FK_NAME,
30+
REGEXP_EXTRACT(pk.constraint_name, r'[^.]+$') AS PK_NAME,
31+
7 AS DEFERRABILITY
32+
FROM `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` AS pk
33+
JOIN `%1$s.%2$s.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE` AS ccu
34+
ON ccu.table_catalog = pk.table_catalog
35+
AND ccu.table_schema = pk.table_schema
36+
AND ccu.table_name = pk.table_name
37+
AND ccu.column_name = pk.column_name
38+
JOIN `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` AS fk
39+
ON fk.constraint_catalog = ccu.constraint_catalog
40+
AND fk.constraint_schema = ccu.constraint_schema
41+
AND fk.constraint_name = ccu.constraint_name
42+
AND fk.position_in_unique_constraint = pk.ordinal_position
43+
JOIN `%1$s.%2$s.INFORMATION_SCHEMA.TABLE_CONSTRAINTS` AS tc
44+
ON tc.constraint_catalog = fk.constraint_catalog
45+
AND tc.constraint_schema = fk.constraint_schema
46+
AND tc.constraint_name = fk.constraint_name
47+
AND tc.constraint_type = 'FOREIGN KEY'
48+
WHERE CASE WHEN pk.table_name = '%3$s' THEN 1 ELSE 0 END = 1
49+
AND pk.position_in_unique_constraint IS NULL
50+
ORDER BY fk.table_schema, fk.table_name, fk.ordinal_position

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.io.IOException;
3838
import java.io.InputStream;
3939
import java.sql.DatabaseMetaData;
40+
import java.sql.PreparedStatement;
4041
import java.sql.ResultSet;
4142
import java.sql.ResultSetMetaData;
4243
import java.sql.SQLException;
@@ -3522,7 +3523,27 @@ public void testGetImportedKeys_noKeys() throws SQLException {
35223523
}
35233524

35243525
@Test
3525-
public void testGetExportedKeys_hasKeys() throws SQLException {
3526+
public void testGetExportedKeys_infoSchema() throws SQLException {
3527+
PreparedStatement mockStmt = mock(PreparedStatement.class);
3528+
ResultSet mockRs = mock(ResultSet.class);
3529+
when(bigQueryConnection.prepareStatement(anyString())).thenReturn(mockStmt);
3530+
when(mockStmt.executeQuery()).thenReturn(mockRs);
3531+
3532+
ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset_p", "ref_table");
3533+
assertEquals(mockRs, rs);
3534+
verify(mockStmt).closeOnCompletion();
3535+
verify(mockStmt).executeQuery();
3536+
}
3537+
3538+
@Test
3539+
public void testGetExportedKeys_pcntSchema() throws SQLException {
3540+
try (ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset.p", "ref_table")) {
3541+
assertFalse(rs.next());
3542+
}
3543+
}
3544+
3545+
@Test
3546+
public void testGetExportedKeys_fallback_hasKeys() throws SQLException {
35263547
DatasetId datasetId = DatasetId.of("test-project", "dataset_p");
35273548
TableId tableId = TableId.of("test-project", "dataset_p", "table_p");
35283549
TableId refTableId = TableId.of("test-project", "dataset_p", "ref_table");
@@ -3544,7 +3565,7 @@ public void testGetExportedKeys_hasKeys() throws SQLException {
35443565
mockDatasetIteration(datasetId);
35453566
mockTableIteration(datasetId, mockTableP);
35463567

3547-
try (ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset_p", "ref_table")) {
3568+
try (ResultSet rs = dbMetadata.getExportedKeys("test-project", null, "ref_table")) {
35483569
assertTrue(rs.next());
35493570
assertEquals("test-project", rs.getString("PKTABLE_CAT"));
35503571
assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM"));
@@ -3572,15 +3593,15 @@ public void testGetExportedKeys_hasKeys() throws SQLException {
35723593
}
35733594

35743595
@Test
3575-
public void testGetExportedKeys_noKeys() throws SQLException {
3596+
public void testGetExportedKeys_fallback_noKeys() throws SQLException {
35763597
DatasetId datasetId = DatasetId.of("test-project", "dataset_p");
35773598
TableId tableId = TableId.of("test-project", "dataset_p", "table_p");
35783599

35793600
Table mockTableP = mockTableWithConstraints(tableId, null);
35803601
mockDatasetIteration(datasetId);
35813602
mockTableIteration(datasetId, mockTableP);
35823603

3583-
try (ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset_p", "ref_table")) {
3604+
try (ResultSet rs = dbMetadata.getExportedKeys("test-project", null, "ref_table")) {
35843605
assertFalse(rs.next());
35853606
}
35863607
}

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public class ITDatabaseMetadataTest extends ITBase {
5757
private static final String CONSTRAINTS_TABLE_NAME = "JDBC_CONSTRAINTS_TEST_TABLE";
5858
private static final String CONSTRAINTS_TABLE_NAME2 = "JDBC_CONSTRAINTS_TEST_TABLE2";
5959
private static final String CONSTRAINTS_TABLE_NAME3 = "JDBC_CONSTRAINTS_TEST_TABLE3";
60+
private static final String PCNT_SCHEMA = "bq-drivers-test-warehouse.jdbc_pcnt_test_namespace";
61+
private static final String PCNT_TABLE_NAME = "PCNT_TEST_TABLE";
6062
private static final Pattern VERSION_PATTERN =
6163
Pattern.compile("^(\\d+)\\.(\\d+)(?:\\.\\d+)+\\s*.*");
6264
private static final String DEFAULT_CATALOG = ServiceOptions.getDefaultProjectId();

0 commit comments

Comments
 (0)