Skip to content

Commit d85fb10

Browse files
fix(bigquery-jdbc): align metadata methods error handling with spec (#13793)
b/534730957 This PR aligns the Google BigQuery JDBC driver's error handling for metadata searches with the JDBC specification and matches general JDBC drivers. The `ignoreAccessErrors` parameter was removed, as error handling is now correctly dictated by the API error code and resource type rather than the search scope. * **Graceful 404 Swallowing for Tables/Routines**: `404 Not Found` API errors encountered when listing tables/routines (e.g. from a deleted dataset) or performing targeted table lookups are now treated as search misses. The driver unconditionally swallows the 404 and successfully returns an empty `ResultSet` (0 rows) per JDBC spec. * **Strict 403 Enforcement**: `403 Forbidden` access errors are no longer silently swallowed during broad scans. Any access denial immediately aborts execution and throws a `SQLException`, strictly adhering to the JDBC specification and other JDBC drivers. * **Project-Level 404 Throwing**: If a broad *dataset* scan fails with a `404 Not Found` (indicating the parent project is inaccessible or missing), the driver explicitly throws a `SQLException` to emulate general JDBC drivers behaviour
1 parent 62c22b7 commit d85fb10

3 files changed

Lines changed: 92 additions & 59 deletions

File tree

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

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ public ResultSet getProcedures(
836836
(rt) -> rt.getRoutineId().getRoutine(),
837837
procedureNamePattern,
838838
procedureNameRegex,
839-
LOG);
839+
false);
840840
Future<List<Routine>> apiFuture = apiExecutor.submit(apiCallable);
841841
apiFutures.add(apiFuture);
842842
}
@@ -1138,7 +1138,7 @@ List<RoutineId> listMatchingProcedureIdsFromDatasets(
11381138
(rt) -> rt.getRoutineId().getRoutine(),
11391139
procedureNamePattern,
11401140
procedureNameRegex,
1141-
logger);
1141+
false);
11421142
listRoutineFutures.add(listRoutinesExecutor.submit(listCallable));
11431143
}
11441144
logger.fine(
@@ -1637,7 +1637,7 @@ public ResultSet getTables(
16371637
(tbl) -> tbl.getTableId().getTable(),
16381638
tableNamePattern,
16391639
tableNameRegex,
1640-
LOG);
1640+
false);
16411641
Future<List<Table>> apiFuture = apiExecutor.submit(apiCallable);
16421642
apiFutures.add(apiFuture);
16431643
}
@@ -1951,7 +1951,7 @@ public ResultSet getColumns(
19511951
(tbl) -> tbl.getTableId().getTable(),
19521952
tableNamePattern,
19531953
tableNameRegex,
1954-
LOG);
1954+
false);
19551955

19561956
for (Table table : tablesToScan) {
19571957
if (Thread.currentThread().isInterrupted()) {
@@ -2428,13 +2428,11 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr
24282428
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
24292429
List<DatasetId> targetDatasets = getTargetDatasets(catalog, schema);
24302430

2431-
boolean ignoreAccessErrors = (catalog == null);
24322431
processTargetTablesConcurrently(
24332432
targetDatasets,
24342433
table,
24352434
collectedResults,
24362435
resultSchemaFields,
2437-
ignoreAccessErrors,
24382436
(bqTable, results, fields) -> {
24392437
TableConstraints constraints = bqTable.getTableConstraints();
24402438
processPrimaryKey(constraints, bqTable.getTableId(), results, fields);
@@ -2525,13 +2523,11 @@ public ResultSet getImportedKeys(String catalog, String schema, String table)
25252523
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
25262524
List<DatasetId> targetDatasets = getTargetDatasets(catalog, schema);
25272525

2528-
boolean ignoreAccessErrors = (catalog == null);
25292526
processTargetTablesConcurrently(
25302527
targetDatasets,
25312528
table,
25322529
collectedResults,
25332530
resultSchemaFields,
2534-
ignoreAccessErrors,
25352531
(bqTable, results, fields) -> {
25362532
TableConstraints constraints = bqTable.getTableConstraints();
25372533
if (constraints == null || constraints.getForeignKeys() == null) {
@@ -2578,15 +2574,13 @@ public ResultSet getExportedKeys(String catalog, String schema, String table)
25782574
// Fallback Path: If catalog or schema is null, fall back to REST API metadata scan.
25792575
if (catalog == null || schema == null) {
25802576
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
2581-
List<DatasetId> targetDatasets = getTargetDatasets(catalog, schema);
2577+
List<DatasetId> targetDatasets = getTargetDatasets(catalog, null);
25822578

2583-
boolean ignoreAccessErrors = (catalog == null);
25842579
processTargetTablesConcurrently(
25852580
targetDatasets,
25862581
null,
25872582
collectedResults,
25882583
resultSchemaFields,
2589-
ignoreAccessErrors,
25902584
(bqTable, results, fields) -> {
25912585
TableConstraints constraints = bqTable.getTableConstraints();
25922586
if (constraints == null || constraints.getForeignKeys() == null) {
@@ -2656,13 +2650,11 @@ public ResultSet getCrossReference(
26562650
final List<FieldValueList> collectedResults = Collections.synchronizedList(new ArrayList<>());
26572651
List<DatasetId> targetDatasets = getTargetDatasets(foreignCatalog, foreignSchema);
26582652

2659-
boolean ignoreAccessErrors = (foreignCatalog == null);
26602653
processTargetTablesConcurrently(
26612654
targetDatasets,
26622655
foreignTable,
26632656
collectedResults,
26642657
resultSchemaFields,
2665-
ignoreAccessErrors,
26662658
(bqTable, results, fields) -> {
26672659
TableConstraints constraints = bqTable.getTableConstraints();
26682660
if (constraints == null || constraints.getForeignKeys() == null) {
@@ -3869,7 +3861,7 @@ public ResultSet getFunctions(String catalog, String schemaPattern, String funct
38693861
(rt) -> rt.getRoutineId().getRoutine(),
38703862
functionNamePattern,
38713863
functionNameRegex,
3872-
LOG);
3864+
false);
38733865
};
38743866
Future<List<Routine>> apiFuture = apiExecutor.submit(apiCallable);
38753867
apiFutures.add(apiFuture);
@@ -4220,7 +4212,7 @@ List<RoutineId> listMatchingFunctionIdsFromDatasets(
42204212
(rt) -> rt.getRoutineId().getRoutine(),
42214213
functionNamePattern,
42224214
functionNameRegex,
4223-
logger);
4215+
false);
42244216
listRoutineFutures.add(listRoutinesExecutor.submit(listCallable));
42254217
}
42264218
logger.fine(
@@ -4656,7 +4648,7 @@ <T> List<T> findMatchingBigQueryObjects(
46564648
Function<T, String> nameExtractor,
46574649
String pattern,
46584650
Pattern regex,
4659-
BigQueryJdbcCustomLogger logger)
4651+
boolean throwOn404)
46604652
throws InterruptedException {
46614653

46624654
boolean needsList = needsListing(pattern);
@@ -4665,30 +4657,29 @@ <T> List<T> findMatchingBigQueryObjects(
46654657
try {
46664658
Iterable<T> objects;
46674659
if (needsList) {
4668-
logger.info(
4660+
LOG.info(
46694661
"Listing all %ss (pattern: %s)...",
46704662
objectTypeName, pattern == null ? "<all>" : pattern);
46714663
Page<T> firstPage = listAllOperation.get();
46724664
objects = firstPage.iterateAll();
4673-
logger.fine(
4674-
"Retrieved initial %s list, iterating & filtering if needed...", objectTypeName);
4665+
LOG.fine("Retrieved initial %s list, iterating & filtering if needed...", objectTypeName);
46754666

46764667
} else {
4677-
logger.info("Getting specific %s: '%s'", objectTypeName, pattern);
4668+
LOG.info("Getting specific %s: '%s'", objectTypeName, pattern);
46784669
T specificObject = getSpecificOperation.apply(pattern);
46794670
objects =
46804671
(specificObject == null)
46814672
? Collections.<T>emptyList()
46824673
: Collections.singletonList(specificObject);
46834674
if (specificObject == null) {
4684-
logger.info("Specific %s not found: '%s'", objectTypeName, pattern);
4675+
LOG.info("Specific %s not found: '%s'", objectTypeName, pattern);
46854676
}
46864677
}
46874678

46884679
boolean wasListing = needsList;
46894680
for (T obj : objects) {
46904681
if (Thread.currentThread().isInterrupted()) {
4691-
logger.warning("Thread interrupted during " + objectTypeName + " processing loop.");
4682+
LOG.warning("Thread interrupted during " + objectTypeName + " processing loop.");
46924683
throw new InterruptedException(
46934684
"Interrupted during " + objectTypeName + " processing loop");
46944685
}
@@ -4705,20 +4696,21 @@ <T> List<T> findMatchingBigQueryObjects(
47054696
}
47064697

47074698
} catch (BigQueryException e) {
4708-
if (!needsList && e.getCode() == 404) {
4709-
logger.info("%s '%s' not found (API error 404).", objectTypeName, pattern);
4699+
if (e.getCode() == 404 && !throwOn404) {
4700+
LOG.info("%s '%s' not found (API error 404).", objectTypeName, pattern);
4701+
return Collections.emptyList();
47104702
} else {
4711-
logger.warning(
4703+
LOG.warning(
47124704
"BigQueryException finding %ss for pattern '%s': %s (Code: %d)",
47134705
objectTypeName, pattern, e.getMessage(), e.getCode());
47144706
throw e;
47154707
}
47164708
} catch (InterruptedException e) {
47174709
Thread.currentThread().interrupt();
4718-
logger.warning("Interrupted while finding " + objectTypeName + "s.");
4710+
LOG.warning("Interrupted while finding " + objectTypeName + "s.");
47194711
throw e;
47204712
} catch (Exception e) {
4721-
logger.severe(
4713+
LOG.severe(
47224714
"Unexpected exception finding %ss for pattern '%s': %s",
47234715
objectTypeName, pattern, e.getMessage());
47244716
throw new RuntimeException(e);
@@ -5022,7 +5014,8 @@ private void signalEndOfData(
50225014
}
50235015

50245016
private List<Dataset> fetchDatasetsForProject(
5025-
String project, String schemaPattern, Pattern schemaRegex) throws SQLException {
5017+
String project, String schemaPattern, Pattern schemaRegex, boolean throwOn404)
5018+
throws SQLException {
50265019
try {
50275020
List<Dataset> datasets =
50285021
findMatchingBigQueryObjects(
@@ -5032,7 +5025,7 @@ private List<Dataset> fetchDatasetsForProject(
50325025
(ds) -> ds.getDatasetId().getDataset(),
50335026
schemaPattern,
50345027
schemaRegex,
5035-
LOG);
5028+
throwOn404);
50365029
return datasets != null ? datasets : Collections.emptyList();
50375030
} catch (InterruptedException e) {
50385031
Thread.currentThread().interrupt();
@@ -5052,9 +5045,11 @@ private List<Dataset> fetchMatchingDatasets(
50525045
return Collections.emptyList();
50535046
}
50545047

5048+
boolean isBroadScan = (catalog == null);
5049+
50555050
// Single project path
50565051
if (projects.size() == 1) {
5057-
return fetchDatasetsForProject(projects.get(0), schemaPattern, schemaRegex);
5052+
return fetchDatasetsForProject(projects.get(0), schemaPattern, schemaRegex, isBroadScan);
50585053
}
50595054

50605055
// Multi-project path
@@ -5066,7 +5061,8 @@ private List<Dataset> fetchMatchingDatasets(
50665061
for (String project : projects) {
50675062
Callable<Void> task =
50685063
() -> {
5069-
List<Dataset> datasets = fetchDatasetsForProject(project, schemaPattern, schemaRegex);
5064+
List<Dataset> datasets =
5065+
fetchDatasetsForProject(project, schemaPattern, schemaRegex, isBroadScan);
50705066
allDatasets.addAll(datasets);
50715067
return null;
50725068
};
@@ -5179,18 +5175,17 @@ private void processSingleTable(
51795175
String tableName,
51805176
List<FieldValueList> collectedResults,
51815177
FieldList resultSchemaFields,
5182-
boolean ignoreAccessErrors,
51835178
TableProcessor processor)
51845179
throws SQLException {
51855180
Table bqTable;
51865181
try {
51875182
bqTable =
51885183
bigquery.getTable(TableId.of(datasetId.getProject(), datasetId.getDataset(), tableName));
51895184
} catch (BigQueryException e) {
5190-
if (ignoreAccessErrors && (e.getCode() == 404 || e.getCode() == 403)) {
5185+
if (e.getCode() == 404) {
51915186
LOG.info(
5192-
"Table '%s' or dataset '%s' not found/accessible in project '%s' (API error %d). Skipping.",
5193-
tableName, datasetId.getDataset(), datasetId.getProject(), e.getCode());
5187+
"Table '%s' or dataset '%s' not found in project '%s' (API error 404). Skipping.",
5188+
tableName, datasetId.getDataset(), datasetId.getProject());
51945189
bqTable = null;
51955190
} else {
51965191
throw new SQLException("Error while fetching table metadata: " + e.getMessage(), e);
@@ -5208,17 +5203,11 @@ private void processTargetTablesConcurrently(
52085203
String tableName,
52095204
List<FieldValueList> collectedResults,
52105205
FieldList resultSchemaFields,
5211-
boolean ignoreAccessErrors,
52125206
TableProcessor processor)
52135207
throws SQLException {
52145208
if (targetDatasets.size() == 1 && tableName != null) {
52155209
processSingleTable(
5216-
targetDatasets.get(0),
5217-
tableName,
5218-
collectedResults,
5219-
resultSchemaFields,
5220-
ignoreAccessErrors,
5221-
processor);
5210+
targetDatasets.get(0), tableName, collectedResults, resultSchemaFields, processor);
52225211
return;
52235212
}
52245213

@@ -5232,12 +5221,7 @@ private void processTargetTablesConcurrently(
52325221
tasks.add(
52335222
() -> {
52345223
processSingleTable(
5235-
datasetId,
5236-
tableName,
5237-
collectedResults,
5238-
resultSchemaFields,
5239-
ignoreAccessErrors,
5240-
processor);
5224+
datasetId, tableName, collectedResults, resultSchemaFields, processor);
52415225
return null;
52425226
});
52435227
continue;
@@ -5261,16 +5245,15 @@ private void processTargetTablesConcurrently(
52615245
table.getTableId().getTable(),
52625246
collectedResults,
52635247
resultSchemaFields,
5264-
ignoreAccessErrors,
52655248
processor);
52665249
return null;
52675250
});
52685251
}
52695252
} catch (BigQueryException e) {
5270-
if (ignoreAccessErrors && (e.getCode() == 404 || e.getCode() == 403)) {
5253+
if (e.getCode() == 404) {
52715254
LOG.info(
5272-
"Dataset '%s' not found/accessible in project '%s' (API error %d). Skipping.",
5273-
datasetId.getDataset(), datasetId.getProject(), e.getCode());
5255+
"Dataset '%s' not found in project '%s' (API error 404). Skipping.",
5256+
datasetId.getDataset(), datasetId.getProject());
52745257
continue;
52755258
}
52765259
throw new SQLException("Error while listing tables: " + e.getMessage(), e);

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListWithPattern() throws Ex
10281028
(rt) -> rt.getRoutineId().getRoutine(),
10291029
pattern,
10301030
regex,
1031-
dbMetadata.LOG);
1031+
false);
10321032

10331033
verify(bigqueryClient, times(1))
10341034
.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class));
@@ -1070,7 +1070,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListNoPattern() throws Exce
10701070
(rt) -> rt.getRoutineId().getRoutine(),
10711071
pattern,
10721072
regex,
1073-
dbMetadata.LOG);
1073+
false);
10741074

10751075
verify(bigqueryClient, times(1))
10761076
.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class));
@@ -1105,7 +1105,7 @@ public void testFindMatchingBigQueryObjects_Routines_GetSpecific() throws Except
11051105
(rt) -> rt.getRoutineId().getRoutine(),
11061106
procNameExact,
11071107
regex,
1108-
dbMetadata.LOG);
1108+
false);
11091109

11101110
verify(bigqueryClient, times(1)).getRoutine(eq(routineId));
11111111
verify(bigqueryClient, never())
@@ -1117,6 +1117,57 @@ public void testFindMatchingBigQueryObjects_Routines_GetSpecific() throws Except
11171117
assertSame(mockRoutine, resultList.get(0));
11181118
}
11191119

1120+
private List<Table> invokeFindMatchingObjectsWithException(
1121+
BigQueryException bqe, String pattern, boolean throwOn404) throws Exception {
1122+
return dbMetadata.findMatchingBigQueryObjects(
1123+
"Table",
1124+
() -> {
1125+
throw bqe;
1126+
},
1127+
(name) -> {
1128+
throw bqe;
1129+
},
1130+
(table) -> "name",
1131+
pattern,
1132+
dbMetadata.compileSqlLikePattern(pattern),
1133+
throwOn404);
1134+
}
1135+
1136+
@Test
1137+
public void testFindMatchingBigQueryObjects_Swallows404_TargetedScan() throws Exception {
1138+
List<Table> results =
1139+
invokeFindMatchingObjectsWithException(
1140+
new BigQueryException(404, "Not Found"), "exact_match", false);
1141+
assertTrue(results.isEmpty());
1142+
}
1143+
1144+
@Test
1145+
public void testFindMatchingBigQueryObjects_Throws404_BroadScan() {
1146+
assertThrows(
1147+
BigQueryException.class,
1148+
() ->
1149+
invokeFindMatchingObjectsWithException(
1150+
new BigQueryException(404, "Not Found"), "%", true));
1151+
}
1152+
1153+
@Test
1154+
public void testFindMatchingBigQueryObjects_Throws403_BroadScan() {
1155+
assertThrows(
1156+
BigQueryException.class,
1157+
() ->
1158+
invokeFindMatchingObjectsWithException(
1159+
new BigQueryException(403, "Access Denied"), "%", true));
1160+
}
1161+
1162+
@Test
1163+
public void testFindMatchingBigQueryObjects_Throws403_TargetedScan() {
1164+
assertThrows(
1165+
BigQueryException.class,
1166+
() ->
1167+
invokeFindMatchingObjectsWithException(
1168+
new BigQueryException(403, "Access Denied"), "exact_match", false));
1169+
}
1170+
11201171
@Test
11211172
public void testDefineGetProcedureColumnsSchema() {
11221173
Schema schema = dbMetadata.defineGetProcedureColumnsSchema();

0 commit comments

Comments
 (0)