Skip to content

Commit 1e05031

Browse files
feat(bigquery-jdbc): migrate getExportedKeys to BQ API (#13711)
b/532245343 This PR migrates `BigQueryDatabaseMetaData.getExportedKeys()` from legacy `INFORMATION_SCHEMA` SQL queries to the native BigQuery Java SDK. This leverages the `TableConstraints` metadata directly, aligning it with the recent migrations of `getPrimaryKeys` and `getImportedKeys`. **Key Changes:** * **API Migration**: Rewrote `getExportedKeys` to use `TableConstraints` fetched via the native SDK, eliminating the need for `DatabaseMetaData_GetExportedKeys.sql`. * **Concurrent Scanning**: Upgraded `processTargetTablesConcurrently` to dynamically list and concurrently scan all tables across target datasets when a parent table isn't explicitly provided. * **Sorting Compliance**: Added `defineFkTableSortComparator` to correctly sort result sets by `FKTABLE_CAT`, `FKTABLE_SCHEM`, `FKTABLE_NAME`, and `KEY_SEQ` as strictly required by the JDBC spec. * **Code Cleanup**: Removed obsolete SQL constants, `.sql` resource files, and dead helper methods (`readSqlFromFile`, `replaceSqlParameters`). * **Testing**: Added new integration test `testGetExportedKeys`
1 parent 9dad6e1 commit 1e05031

3 files changed

Lines changed: 176 additions & 144 deletions

File tree

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

Lines changed: 118 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,18 @@
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.BufferedReader;
56-
import java.io.InputStream;
57-
import java.io.InputStreamReader;
5855
import java.sql.Connection;
5956
import java.sql.DatabaseMetaData;
6057
import java.sql.ResultSet;
6158
import java.sql.RowIdLifetime;
6259
import java.sql.SQLException;
63-
import java.sql.Statement;
6460
import java.sql.Types;
6561
import java.util.ArrayList;
6662
import java.util.Arrays;
6763
import java.util.Collections;
6864
import java.util.Comparator;
6965
import java.util.HashSet;
7066
import java.util.List;
71-
import java.util.Scanner;
7267
import java.util.Set;
7368
import java.util.concurrent.BlockingQueue;
7469
import java.util.concurrent.Callable;
@@ -89,17 +84,14 @@
8984
*
9085
* @see BigQueryStatement
9186
*/
92-
// TODO(neenu): test and verify after post MVP implementation.
9387
class BigQueryDatabaseMetaData implements DatabaseMetaData {
9488
final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString());
9589
private static final String DATABASE_PRODUCT_NAME = "Google BigQuery";
9690
private static final String DATABASE_PRODUCT_VERSION = "2.0";
9791
private static final String DRIVER_NAME = "GoogleJDBCDriverForGoogleBigQuery";
98-
9992
private static final String SCHEMA_TERM = "Dataset";
10093
private static final String CATALOG_TERM = "Project";
10194
private static final String PROCEDURE_TERM = "Procedure";
102-
private static final String GET_EXPORTED_KEYS_SQL = "DatabaseMetaData_GetExportedKeys.sql";
10395
private static final int DEFAULT_PAGE_SIZE = 500;
10496
private static final int DEFAULT_QUEUE_CAPACITY = 5000;
10597
// Declared package-private for testing.
@@ -1820,11 +1812,9 @@ public ResultSet getCatalogs() throws SQLException {
18201812
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
18211813
new LinkedBlockingQueue<>(catalogRows.isEmpty() ? 1 : catalogRows.size() + 1);
18221814

1823-
populateQueue(catalogRows, queue, schemaFields);
1824-
signalEndOfData(queue, schemaFields);
1815+
Future<?> fetcherFuture = populateQueueAsync(catalogRows, queue, schemaFields);
18251816

1826-
return BigQueryJsonResultSet.of(
1827-
catalogsSchema, catalogRows.size(), queue, null, new Future<?>[0]);
1817+
return BigQueryJsonResultSet.of(catalogsSchema, catalogRows.size(), queue, null, fetcherFuture);
18281818
}
18291819

18301820
Schema defineGetCatalogsSchema() {
@@ -1852,11 +1842,11 @@ public ResultSet getTableTypes() {
18521842
BlockingQueue<BigQueryFieldValueListWrapper> queue =
18531843
new LinkedBlockingQueue<>(tableTypeRows.size() + 1);
18541844

1855-
populateQueue(tableTypeRows, queue, tableTypesSchema.getFields());
1856-
signalEndOfData(queue, tableTypesSchema.getFields());
1845+
Future<?> fetcherFuture =
1846+
populateQueueAsync(tableTypeRows, queue, tableTypesSchema.getFields());
18571847

18581848
return BigQueryJsonResultSet.of(
1859-
tableTypesSchema, tableTypeRows.size(), queue, null, new Future<?>[0]);
1849+
tableTypesSchema, tableTypeRows.size(), queue, null, fetcherFuture);
18601850
}
18611851

18621852
static Schema defineGetTableTypesSchema() {
@@ -2413,17 +2403,6 @@ Schema defineGetVersionColumnsSchema() {
24132403
return Schema.of(fields);
24142404
}
24152405

2416-
private void closeStatementIgnoreException(Statement statement) {
2417-
if (statement == null) {
2418-
return;
2419-
}
2420-
try {
2421-
statement.close();
2422-
} catch (SQLException e) {
2423-
// pass
2424-
}
2425-
}
2426-
24272406
@Override
24282407
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
24292408
if ((catalog != null && catalog.isEmpty())
@@ -2458,9 +2437,8 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr
24582437

24592438
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
24602439
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2461-
populateQueue(collectedResults, queue, resultSchemaFields);
2462-
signalEndOfData(queue, resultSchemaFields);
2463-
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
2440+
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
2441+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
24642442
}
24652443

24662444
private Schema defineGetPrimaryKeysSchema() {
@@ -2562,24 +2540,59 @@ public ResultSet getImportedKeys(String catalog, String schema, String table)
25622540

25632541
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
25642542
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2565-
populateQueue(collectedResults, queue, resultSchemaFields);
2566-
signalEndOfData(queue, resultSchemaFields);
2567-
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
2543+
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
2544+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
25682545
}
25692546

25702547
@Override
25712548
public ResultSet getExportedKeys(String catalog, String schema, String table)
25722549
throws SQLException {
2573-
String sql = readSqlFromFile(GET_EXPORTED_KEYS_SQL);
2574-
Statement stmt = this.connection.createStatement();
2575-
try {
2576-
stmt.closeOnCompletion();
2577-
String formattedSql = replaceSqlParameters(sql, catalog, schema, table);
2578-
return stmt.executeQuery(formattedSql);
2579-
} catch (SQLException e) {
2580-
closeStatementIgnoreException(stmt);
2581-
throw new BigQueryJdbcException("Error executing getExportedKeys", e);
2550+
if ((catalog != null && catalog.isEmpty())
2551+
|| (schema != null && schema.isEmpty())
2552+
|| table == null
2553+
|| table.isEmpty()) {
2554+
LOG.warning(
2555+
"Returning empty ResultSet as required parameters are null/empty, or catalog/schema parameters are empty.");
2556+
return new BigQueryJsonResultSet();
25822557
}
2558+
2559+
final Schema resultSchema = defineForeignKeyResultSetSchema();
2560+
final FieldList resultSchemaFields = resultSchema.getFields();
2561+
2562+
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
2563+
List<DatasetId> targetDatasets = getTargetDatasets(catalog, null);
2564+
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;
2584+
}
2585+
processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields);
2586+
}
2587+
});
2588+
2589+
Comparator<FieldValueList> comparator = defineFkTableSortComparator(resultSchemaFields);
2590+
sortResults(collectedResults, comparator, "getExportedKeys", LOG);
2591+
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);
25832596
}
25842597

25852598
@Override
@@ -2638,9 +2651,8 @@ public ResultSet getCrossReference(
26382651

26392652
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
26402653
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2641-
populateQueue(collectedResults, queue, resultSchemaFields);
2642-
signalEndOfData(queue, resultSchemaFields);
2643-
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
2654+
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
2655+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
26442656
}
26452657

26462658
@Override
@@ -2656,10 +2668,9 @@ public ResultSet getTypeInfo() {
26562668
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
26572669
new LinkedBlockingQueue<>(typeInfoRows.size() + 1);
26582670

2659-
populateQueue(typeInfoRows, queue, schemaFields);
2660-
signalEndOfData(queue, schemaFields);
2671+
Future<?> fetcherFuture = populateQueueAsync(typeInfoRows, queue, schemaFields);
26612672
return BigQueryJsonResultSet.of(
2662-
typeInfoSchema, typeInfoRows.size(), queue, null, new Future<?>[0]);
2673+
typeInfoSchema, typeInfoRows.size(), queue, null, fetcherFuture);
26632674
}
26642675

26652676
Schema defineGetTypeInfoSchema() {
@@ -3580,9 +3591,8 @@ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLExce
35803591
}
35813592
Comparator<FieldValueList> comparator = defineGetSchemasComparator(resultSchemaFields);
35823593
sortResults(collectedResults, comparator, "getSchemas", LOG);
3583-
populateQueue(collectedResults, queue, resultSchemaFields);
3584-
signalEndOfData(queue, resultSchemaFields);
3585-
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
3594+
Future<?> fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields);
3595+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture);
35863596
}
35873597

35883598
// Multi-Catalog Path: fan out using connection-scoped metadataExecutor
@@ -4894,6 +4904,19 @@ private void waitForTasksCompletion(List<Future<?>> taskFutures) throws Executio
48944904
LOG.info("Finished waiting for tasks.");
48954905
}
48964906

4907+
private Future<?> populateQueueAsync(
4908+
List<FieldValueList> collectedResults,
4909+
BlockingQueue<BigQueryFieldValueListWrapper> queue,
4910+
FieldList resultSchemaFields) {
4911+
return connection
4912+
.getMetadataExecutor()
4913+
.submit(
4914+
() -> {
4915+
populateQueue(collectedResults, queue, resultSchemaFields);
4916+
signalEndOfData(queue, resultSchemaFields);
4917+
});
4918+
}
4919+
48974920
private void populateQueue(
48984921
List<FieldValueList> collectedResults,
48994922
BlockingQueue<BigQueryFieldValueListWrapper> queue,
@@ -5066,24 +5089,6 @@ private boolean equalsOrNullMatchesAll(String expected, String actual) {
50665089
return expected == null || expected.equals(actual);
50675090
}
50685091

5069-
static String readSqlFromFile(String filename) {
5070-
InputStream in;
5071-
in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename);
5072-
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
5073-
StringBuilder builder = new StringBuilder();
5074-
try (Scanner scanner = new Scanner(reader)) {
5075-
while (scanner.hasNextLine()) {
5076-
String line = scanner.nextLine();
5077-
builder.append(line).append("\n");
5078-
}
5079-
}
5080-
return builder.toString();
5081-
}
5082-
5083-
String replaceSqlParameters(String sql, String... params) throws SQLException {
5084-
return String.format(sql, (Object[]) params);
5085-
}
5086-
50875092
private void writeErrorToQueue(BlockingQueue<BigQueryFieldValueListWrapper> queue, Throwable t) {
50885093
Exception ex = (t instanceof Exception) ? (Exception) t : new Exception(t);
50895094
BigQueryFieldValueListWrapper element = BigQueryFieldValueListWrapper.ofError(ex);
@@ -5171,7 +5176,7 @@ private void processTargetTablesConcurrently(
51715176
boolean ignoreAccessErrors,
51725177
TableProcessor processor)
51735178
throws SQLException {
5174-
if (targetDatasets.size() == 1) {
5179+
if (targetDatasets.size() == 1 && tableName != null) {
51755180
processSingleTable(
51765181
targetDatasets.get(0),
51775182
tableName,
@@ -5186,19 +5191,59 @@ private void processTargetTablesConcurrently(
51865191
List<Future<?>> taskFutures = new ArrayList<>();
51875192

51885193
try {
5194+
List<Callable<Void>> tasks = new ArrayList<>();
51895195
for (DatasetId datasetId : targetDatasets) {
5190-
taskFutures.add(
5191-
executor.submit(
5196+
if (tableName != null) {
5197+
tasks.add(
5198+
() -> {
5199+
processSingleTable(
5200+
datasetId,
5201+
tableName,
5202+
collectedResults,
5203+
resultSchemaFields,
5204+
ignoreAccessErrors,
5205+
processor);
5206+
return null;
5207+
});
5208+
continue;
5209+
}
5210+
5211+
try {
5212+
Page<Table> tablesPage =
5213+
bigquery.listTables(datasetId, TableListOption.pageSize(DEFAULT_PAGE_SIZE));
5214+
if (tablesPage == null) {
5215+
continue;
5216+
}
5217+
for (Table table : tablesPage.iterateAll()) {
5218+
if (table.getDefinition() == null
5219+
|| table.getDefinition().getType() != TableDefinition.Type.TABLE) {
5220+
continue;
5221+
}
5222+
tasks.add(
51925223
() -> {
51935224
processSingleTable(
51945225
datasetId,
5195-
tableName,
5226+
table.getTableId().getTable(),
51965227
collectedResults,
51975228
resultSchemaFields,
51985229
ignoreAccessErrors,
51995230
processor);
52005231
return null;
5201-
}));
5232+
});
5233+
}
5234+
} catch (BigQueryException e) {
5235+
if (ignoreAccessErrors && (e.getCode() == 404 || e.getCode() == 403)) {
5236+
LOG.info(
5237+
"Dataset '%s' not found/accessible in project '%s' (API error %d). Skipping.",
5238+
datasetId.getDataset(), datasetId.getProject(), e.getCode());
5239+
continue;
5240+
}
5241+
throw new SQLException("Error while listing tables: " + e.getMessage(), e);
5242+
}
5243+
}
5244+
5245+
for (Callable<Void> task : tasks) {
5246+
taskFutures.add(executor.submit(task));
52025247
}
52035248
waitForTasksCompletion(taskFutures);
52045249
if (Thread.currentThread().isInterrupted()) {

0 commit comments

Comments
 (0)