Skip to content

Commit 1951f49

Browse files
feat(bigquery-jdbc): migrate getPrimaryKeys to use BQ API (#13691)
b/532245342 Migrates the `getPrimaryKeys` metadata methods in `BigQueryDatabaseMetaData` away from executing SQL queries on `INFORMATION_SCHEMA` tables, replacing them with native BigQuery Java Client SDK calls. This PR also introduces the shared thread-safe concurrent dataset scanning infrastructure that will be utilized by subsequent metadata migrations. ### **Key Changes:** 1. **`getPrimaryKeys`**: Rewritten to fetch metadata using `bigquery.getTable(...)` and parsing table constraints in-memory. 2. **Concurrency Infrastructure**: - Introduced `processTargetTablesConcurrently(...)` and `processSingleTable(...)` to handle concurrent metadata retrieval across target datasets using the metadata executor thread pool. - Added a short-circuit optimization for single target datasets to execute synchronously on the calling thread. - Extracted project list resolution into `resolveTargetProjects(...)` to support wildcard schema matching. 3. **Cleanup**: - Deleted the obsolete `DatabaseMetaData_GetPrimaryKeys.sql` resource file. - Removed two obsolete SQL-parsing unit tests (`testReadSqlFromFile` and `testMetadataMethodsDoNotInterfere`) from `BigQueryDatabaseMetaDataTest.java` as they are no longer applicable.
1 parent a29ea79 commit 1951f49

3 files changed

Lines changed: 211 additions & 88 deletions

File tree

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

Lines changed: 211 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.cloud.bigquery.FieldList;
3535
import com.google.cloud.bigquery.FieldValue;
3636
import com.google.cloud.bigquery.FieldValueList;
37+
import com.google.cloud.bigquery.PrimaryKey;
3738
import com.google.cloud.bigquery.Routine;
3839
import com.google.cloud.bigquery.RoutineArgument;
3940
import com.google.cloud.bigquery.RoutineId;
@@ -42,7 +43,9 @@
4243
import com.google.cloud.bigquery.StandardSQLField;
4344
import com.google.cloud.bigquery.StandardSQLTableType;
4445
import com.google.cloud.bigquery.StandardSQLTypeName;
46+
import com.google.cloud.bigquery.StandardTableDefinition;
4547
import com.google.cloud.bigquery.Table;
48+
import com.google.cloud.bigquery.TableConstraints;
4649
import com.google.cloud.bigquery.TableDefinition;
4750
import com.google.cloud.bigquery.TableId;
4851
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
@@ -2425,16 +2428,102 @@ private void closeStatementIgnoreException(Statement statement) {
24252428

24262429
@Override
24272430
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
2428-
String sql = readSqlFromFile(GET_PRIMARY_KEYS_SQL);
2429-
Statement stmt = this.connection.createStatement();
2430-
try {
2431-
stmt.closeOnCompletion();
2432-
String formattedSql = replaceSqlParameters(sql, catalog, schema, table);
2433-
return stmt.executeQuery(formattedSql);
2434-
} catch (SQLException e) {
2435-
closeStatementIgnoreException(stmt);
2436-
throw new BigQueryJdbcException("Error executing getPrimaryKeys", e);
2431+
if ((catalog != null && catalog.isEmpty())
2432+
|| (schema != null && schema.isEmpty())
2433+
|| table == null
2434+
|| table.isEmpty()) {
2435+
LOG.warning(
2436+
"Returning empty ResultSet as one or more patterns are empty or catalog is empty.");
2437+
return new BigQueryJsonResultSet();
2438+
}
2439+
2440+
final Schema resultSchema = defineGetPrimaryKeysSchema();
2441+
final FieldList resultSchemaFields = resultSchema.getFields();
2442+
2443+
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
2444+
List<DatasetId> targetDatasets = getTargetDatasets(catalog, schema);
2445+
2446+
boolean ignoreAccessErrors = (catalog == null);
2447+
processTargetTablesConcurrently(
2448+
targetDatasets,
2449+
table,
2450+
collectedResults,
2451+
resultSchemaFields,
2452+
ignoreAccessErrors,
2453+
(bqTable, results, fields) -> {
2454+
TableConstraints constraints = null;
2455+
if (bqTable.getDefinition() instanceof StandardTableDefinition) {
2456+
constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints();
2457+
}
2458+
processPrimaryKey(constraints, bqTable.getTableId(), results, fields);
2459+
});
2460+
2461+
Comparator<FieldValueList> comparator = defineGetPrimaryKeysComparator(resultSchemaFields);
2462+
sortResults(collectedResults, comparator, "getPrimaryKeys", LOG);
2463+
2464+
final BlockingQueue<BigQueryFieldValueListWrapper> queue =
2465+
new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
2466+
populateQueue(collectedResults, queue, resultSchemaFields);
2467+
signalEndOfData(queue, resultSchemaFields);
2468+
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
2469+
}
2470+
2471+
private Schema defineGetPrimaryKeysSchema() {
2472+
List<Field> fields = new ArrayList<>(6);
2473+
fields.add(
2474+
Field.newBuilder("TABLE_CAT", StandardSQLTypeName.STRING)
2475+
.setMode(Field.Mode.NULLABLE)
2476+
.build());
2477+
fields.add(
2478+
Field.newBuilder("TABLE_SCHEM", StandardSQLTypeName.STRING)
2479+
.setMode(Field.Mode.NULLABLE)
2480+
.build());
2481+
fields.add(
2482+
Field.newBuilder("TABLE_NAME", StandardSQLTypeName.STRING)
2483+
.setMode(Field.Mode.REQUIRED)
2484+
.build());
2485+
fields.add(
2486+
Field.newBuilder("COLUMN_NAME", StandardSQLTypeName.STRING)
2487+
.setMode(Field.Mode.REQUIRED)
2488+
.build());
2489+
fields.add(
2490+
Field.newBuilder("KEY_SEQ", StandardSQLTypeName.INT64)
2491+
.setMode(Field.Mode.REQUIRED)
2492+
.build());
2493+
fields.add(
2494+
Field.newBuilder("PK_NAME", StandardSQLTypeName.STRING)
2495+
.setMode(Field.Mode.NULLABLE)
2496+
.build());
2497+
return Schema.of(fields);
2498+
}
2499+
2500+
private void processPrimaryKey(
2501+
TableConstraints constraints,
2502+
TableId tableId,
2503+
List<FieldValueList> collectedResults,
2504+
FieldList resultSchemaFields) {
2505+
if (constraints == null || constraints.getPrimaryKey() == null) {
2506+
return;
24372507
}
2508+
PrimaryKey pk = constraints.getPrimaryKey();
2509+
List<String> pkColumns = pk.getColumns();
2510+
for (int i = 0; i < pkColumns.size(); i++) {
2511+
List<FieldValue> row = new ArrayList<>(6);
2512+
row.add(createStringFieldValue(tableId.getProject())); // 1. TABLE_CAT
2513+
row.add(createStringFieldValue(tableId.getDataset())); // 2. TABLE_SCHEM
2514+
row.add(createStringFieldValue(tableId.getTable())); // 3. TABLE_NAME
2515+
row.add(createStringFieldValue(pkColumns.get(i))); // 4. COLUMN_NAME
2516+
row.add(createLongFieldValue((long) (i + 1))); // 5. KEY_SEQ
2517+
row.add(createNullFieldValue()); // 6. PK_NAME
2518+
collectedResults.add(FieldValueList.of(row, resultSchemaFields));
2519+
}
2520+
}
2521+
2522+
private Comparator<FieldValueList> defineGetPrimaryKeysComparator(FieldList resultSchemaFields) {
2523+
final int COLUMN_NAME_IDX = resultSchemaFields.getIndex("COLUMN_NAME");
2524+
return Comparator.comparing(
2525+
(FieldValueList fvl) -> getStringValueOrNull(fvl, COLUMN_NAME_IDX),
2526+
Comparator.nullsFirst(String::compareTo));
24382527
}
24392528

24402529
@Override
@@ -4950,4 +5039,117 @@ private void writeErrorToQueue(BlockingQueue<BigQueryFieldValueListWrapper> queu
49505039
}
49515040
}
49525041
}
5042+
5043+
private List<DatasetId> getTargetDatasets(String catalog, String schema) throws SQLException {
5044+
if (schema == null) {
5045+
List<Dataset> datasets = fetchMatchingDatasets(catalog, null, null);
5046+
List<DatasetId> datasetIds = new ArrayList<>(datasets.size());
5047+
for (Dataset dataset : datasets) {
5048+
datasetIds.add(dataset.getDatasetId());
5049+
}
5050+
return datasetIds;
5051+
}
5052+
5053+
if (schema.isEmpty()) {
5054+
return Collections.emptyList();
5055+
}
5056+
5057+
List<String> projects = resolveTargetProjects(catalog);
5058+
List<DatasetId> datasetIds = new ArrayList<>(projects.size());
5059+
for (String project : projects) {
5060+
datasetIds.add(DatasetId.of(project, schema));
5061+
}
5062+
return datasetIds;
5063+
}
5064+
5065+
private List<String> resolveTargetProjects(String catalog) throws SQLException {
5066+
return (catalog != null) ? Collections.singletonList(catalog) : getAccessibleCatalogNames();
5067+
}
5068+
5069+
@FunctionalInterface
5070+
private interface TableProcessor {
5071+
void process(Table bqTable, List<FieldValueList> collectedResults, FieldList resultSchemaFields)
5072+
throws SQLException;
5073+
}
5074+
5075+
private void processSingleTable(
5076+
DatasetId datasetId,
5077+
String tableName,
5078+
List<FieldValueList> collectedResults,
5079+
FieldList resultSchemaFields,
5080+
boolean ignoreAccessErrors,
5081+
TableProcessor processor)
5082+
throws SQLException {
5083+
Table bqTable;
5084+
try {
5085+
bqTable =
5086+
bigquery.getTable(TableId.of(datasetId.getProject(), datasetId.getDataset(), tableName));
5087+
} catch (BigQueryException e) {
5088+
if (ignoreAccessErrors && (e.getCode() == 404 || e.getCode() == 403)) {
5089+
LOG.info(
5090+
"Table '%s' or dataset '%s' not found/accessible in project '%s' (API error %d). Skipping.",
5091+
tableName, datasetId.getDataset(), datasetId.getProject(), e.getCode());
5092+
bqTable = null;
5093+
} else {
5094+
throw new SQLException("Error while fetching table metadata: " + e.getMessage(), e);
5095+
}
5096+
} catch (Exception e) {
5097+
throw new SQLException("Error while fetching table metadata: " + e.getMessage(), e);
5098+
}
5099+
if (bqTable != null && bqTable.getDefinition() != null) {
5100+
processor.process(bqTable, collectedResults, resultSchemaFields);
5101+
}
5102+
}
5103+
5104+
private void processTargetTablesConcurrently(
5105+
List<DatasetId> targetDatasets,
5106+
String tableName,
5107+
List<FieldValueList> collectedResults,
5108+
FieldList resultSchemaFields,
5109+
boolean ignoreAccessErrors,
5110+
TableProcessor processor)
5111+
throws SQLException {
5112+
if (targetDatasets.size() == 1) {
5113+
processSingleTable(
5114+
targetDatasets.get(0),
5115+
tableName,
5116+
collectedResults,
5117+
resultSchemaFields,
5118+
ignoreAccessErrors,
5119+
processor);
5120+
return;
5121+
}
5122+
5123+
ExecutorService executor = connection.getMetadataExecutor();
5124+
List<Future<?>> taskFutures = new ArrayList<>();
5125+
5126+
try {
5127+
for (DatasetId datasetId : targetDatasets) {
5128+
taskFutures.add(
5129+
executor.submit(
5130+
() -> {
5131+
processSingleTable(
5132+
datasetId,
5133+
tableName,
5134+
collectedResults,
5135+
resultSchemaFields,
5136+
ignoreAccessErrors,
5137+
processor);
5138+
return null;
5139+
}));
5140+
}
5141+
waitForTasksCompletion(taskFutures);
5142+
if (Thread.currentThread().isInterrupted()) {
5143+
throw new SQLException("Interrupted while parallel-fetching metadata");
5144+
}
5145+
} catch (ExecutionException e) {
5146+
Throwable cause = e.getCause();
5147+
if (cause instanceof SQLException) {
5148+
throw (SQLException) cause;
5149+
}
5150+
throw new SQLException("Error while fetching metadata", e);
5151+
} finally {
5152+
taskFutures.forEach(future -> future.cancel(true));
5153+
}
5154+
}
49535155
}

java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetPrimaryKeys.sql

Lines changed: 0 additions & 30 deletions
This file was deleted.

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

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,6 @@ public void testBigqueryDatabaseMetaDataGetters() throws SQLException {
159159
assertEquals("Project", dbMetadata.getCatalogTerm());
160160
}
161161

162-
@Test
163-
public void testReadSqlFromFile() throws SQLException {
164-
BigQueryDatabaseMetaData dbMetadata = new BigQueryDatabaseMetaData(bigQueryConnection);
165-
166-
String primaryKeysQuery =
167-
BigQueryDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql");
168-
assertTrue(primaryKeysQuery.contains("pk$"));
169-
170-
try {
171-
when(bigQueryConnection.prepareStatement(primaryKeysQuery)).thenCallRealMethod();
172-
String sql =
173-
dbMetadata.replaceSqlParameters(
174-
primaryKeysQuery, "project_name", "dataset_name", "table_name");
175-
assertTrue(sql.contains("project_name.dataset_name.INFORMATION_SCHEMA.KEY_COLUMN_USAGE"));
176-
} catch (SQLException e) {
177-
throw new RuntimeException(e);
178-
}
179-
}
180-
181162
@Test
182163
public void testNeedsListing() {
183164
assertTrue(dbMetadata.needsListing(null), "Null pattern should require listing");
@@ -3251,36 +3232,6 @@ public void testWrapperMethods() throws SQLException {
32513232
assertThat((Throwable) e).hasMessageThat().contains("Cannot unwrap to java.sql.Connection");
32523233
}
32533234

3254-
@Test
3255-
public void testMetadataMethodsDoNotInterfere() throws SQLException {
3256-
Statement mockStatement1 = mock(Statement.class);
3257-
Statement mockStatement2 = mock(Statement.class);
3258-
ResultSet mockResultSet1 = mock(ResultSet.class);
3259-
ResultSet mockResultSet2 = mock(ResultSet.class);
3260-
3261-
when(bigQueryConnection.createStatement())
3262-
.thenReturn(mockStatement1)
3263-
.thenReturn(mockStatement2);
3264-
3265-
when(mockStatement1.executeQuery(any())).thenReturn(mockResultSet1);
3266-
when(mockStatement2.executeQuery(any())).thenReturn(mockResultSet2);
3267-
3268-
// Call first metadata method
3269-
ResultSet rs1 = dbMetadata.getPrimaryKeys("cat", "schema", "table");
3270-
assertSame(mockResultSet1, rs1);
3271-
3272-
// Call second metadata method
3273-
ResultSet rs2 = dbMetadata.getImportedKeys("cat", "schema", "table");
3274-
assertSame(mockResultSet2, rs2);
3275-
3276-
// Verify closeOnCompletion was called on both statements
3277-
verify(mockStatement1).closeOnCompletion();
3278-
verify(mockStatement2).closeOnCompletion();
3279-
3280-
// Verify connection.createStatement() was called twice
3281-
verify(bigQueryConnection, times(2)).createStatement();
3282-
}
3283-
32843235
@ParameterizedTest
32853236
@EnumSource(StandardSQLTypeName.class)
32863237
public void testMetadataAndResultSetMetadataTypeMappingConsistency(StandardSQLTypeName type) {

0 commit comments

Comments
 (0)