From 3af7ff2897759600173e31183453f6407d957971 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 7 Jul 2026 23:30:55 +0000 Subject: [PATCH 1/8] feat(bigquery-jdbc): migrate getPrimaryKeys to use BQ API --- .../jdbc/BigQueryDatabaseMetaData.java | 185 +++++++++++++++++- .../jdbc/DatabaseMetaData_GetPrimaryKeys.sql | 30 --- .../jdbc/BigQueryDatabaseMetaDataTest.java | 47 ----- 3 files changed, 176 insertions(+), 86 deletions(-) delete mode 100644 java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetPrimaryKeys.sql diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 752100f4888c..824f1bbb011b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -34,6 +34,7 @@ import com.google.cloud.bigquery.FieldList; import com.google.cloud.bigquery.FieldValue; import com.google.cloud.bigquery.FieldValueList; +import com.google.cloud.bigquery.PrimaryKey; import com.google.cloud.bigquery.Routine; import com.google.cloud.bigquery.RoutineArgument; import com.google.cloud.bigquery.RoutineId; @@ -42,7 +43,9 @@ import com.google.cloud.bigquery.StandardSQLField; import com.google.cloud.bigquery.StandardSQLTableType; import com.google.cloud.bigquery.StandardSQLTypeName; +import com.google.cloud.bigquery.StandardTableDefinition; import com.google.cloud.bigquery.Table; +import com.google.cloud.bigquery.TableConstraints; import com.google.cloud.bigquery.TableDefinition; import com.google.cloud.bigquery.TableId; import com.google.cloud.bigquery.exception.BigQueryJdbcException; @@ -2425,16 +2428,101 @@ private void closeStatementIgnoreException(Statement statement) { @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - String sql = readSqlFromFile(GET_PRIMARY_KEYS_SQL); - Statement stmt = this.connection.createStatement(); - try { - stmt.closeOnCompletion(); - String formattedSql = replaceSqlParameters(sql, catalog, schema, table); - return stmt.executeQuery(formattedSql); - } catch (SQLException e) { - closeStatementIgnoreException(stmt); - throw new BigQueryJdbcException("Error executing getPrimaryKeys", e); + if ((catalog != null && catalog.isEmpty()) + || (schema != null && schema.isEmpty()) + || table == null + || table.isEmpty()) { + LOG.warning( + "Returning empty ResultSet as one or more patterns are empty or catalog is empty."); + return new BigQueryJsonResultSet(); } + + final Schema resultSchema = defineGetPrimaryKeysSchema(); + final FieldList resultSchemaFields = resultSchema.getFields(); + + final List collectedResults = Collections.synchronizedList(new ArrayList<>()); + List targetDatasets = getTargetDatasets(catalog, schema); + + processTargetTablesConcurrently( + targetDatasets, + table, + collectedResults, + resultSchemaFields, + (bqTable, results, fields) -> { + TableConstraints constraints = null; + if (bqTable.getDefinition() instanceof StandardTableDefinition) { + constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); + } + processPrimaryKey(constraints, bqTable.getTableId(), results, fields); + }); + + Comparator comparator = defineGetPrimaryKeysComparator(resultSchemaFields); + sortResults(collectedResults, comparator, "getPrimaryKeys", LOG); + + final BlockingQueue queue = + new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + populateQueue(collectedResults, queue, resultSchemaFields); + signalEndOfData(queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); + } + + Schema defineGetPrimaryKeysSchema() { + List fields = new ArrayList<>(6); + fields.add( + Field.newBuilder("TABLE_CAT", StandardSQLTypeName.STRING) + .setMode(Field.Mode.NULLABLE) + .build()); + fields.add( + Field.newBuilder("TABLE_SCHEM", StandardSQLTypeName.STRING) + .setMode(Field.Mode.NULLABLE) + .build()); + fields.add( + Field.newBuilder("TABLE_NAME", StandardSQLTypeName.STRING) + .setMode(Field.Mode.REQUIRED) + .build()); + fields.add( + Field.newBuilder("COLUMN_NAME", StandardSQLTypeName.STRING) + .setMode(Field.Mode.REQUIRED) + .build()); + fields.add( + Field.newBuilder("KEY_SEQ", StandardSQLTypeName.INT64) + .setMode(Field.Mode.REQUIRED) + .build()); + fields.add( + Field.newBuilder("PK_NAME", StandardSQLTypeName.STRING) + .setMode(Field.Mode.NULLABLE) + .build()); + return Schema.of(fields); + } + + void processPrimaryKey( + TableConstraints constraints, + TableId tableId, + List collectedResults, + FieldList resultSchemaFields) { + if (constraints != null && constraints.getPrimaryKey() != null) { + PrimaryKey pk = constraints.getPrimaryKey(); + List pkColumns = pk.getColumns(); + for (int i = 0; i < pkColumns.size(); i++) { + List row = new ArrayList<>(6); + row.add( + FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getProject())); // 1. TABLE_CAT + row.add( + FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getDataset())); // 2. TABLE_SCHEM + row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getTable())); // 3. TABLE_NAME + row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, pkColumns.get(i))); // 4. COLUMN_NAME + row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, String.valueOf(i + 1))); // 5. KEY_SEQ + row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, null)); // 6. PK_NAME + collectedResults.add(FieldValueList.of(row, resultSchemaFields)); + } + } + } + + Comparator defineGetPrimaryKeysComparator(FieldList resultSchemaFields) { + final int COLUMN_NAME_IDX = resultSchemaFields.getIndex("COLUMN_NAME"); + return Comparator.comparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, COLUMN_NAME_IDX), + Comparator.nullsFirst(String::compareTo)); } @Override @@ -4950,4 +5038,83 @@ private void writeErrorToQueue(BlockingQueue queu } } } + + private List getTargetDatasets(String catalog, String schema) throws SQLException { + if (schema == null) { + List datasets = fetchMatchingDatasets(catalog, null, null); + List datasetIds = new ArrayList<>(datasets.size()); + for (Dataset dataset : datasets) { + datasetIds.add(dataset.getDatasetId()); + } + return datasetIds; + } + + if (schema.isEmpty()) { + return Collections.emptyList(); + } + + List projects = resolveTargetProjects(catalog); + List datasetIds = new ArrayList<>(projects.size()); + for (String project : projects) { + datasetIds.add(DatasetId.of(project, schema)); + } + return datasetIds; + } + + private List resolveTargetProjects(String catalog) throws SQLException { + return (catalog != null) ? Collections.singletonList(catalog) : getAccessibleCatalogNames(); + } + + @FunctionalInterface + private interface TableProcessor { + void process(Table bqTable, List collectedResults, FieldList resultSchemaFields) + throws SQLException; + } + + private void processSingleTable( + DatasetId datasetId, + String tableName, + List collectedResults, + FieldList resultSchemaFields, + TableProcessor processor) + throws SQLException { + Table bqTable = + bigquery.getTable(TableId.of(datasetId.getProject(), datasetId.getDataset(), tableName)); + if (bqTable != null && bqTable.getDefinition() != null) { + processor.process(bqTable, collectedResults, resultSchemaFields); + } + } + + private void processTargetTablesConcurrently( + List targetDatasets, + String tableName, + List collectedResults, + FieldList resultSchemaFields, + TableProcessor processor) + throws SQLException { + if (targetDatasets.size() == 1) { + processSingleTable( + targetDatasets.get(0), tableName, collectedResults, resultSchemaFields, processor); + return; + } + + ExecutorService executor = connection.getMetadataExecutor(); + List> taskFutures = new ArrayList<>(); + + for (DatasetId datasetId : targetDatasets) { + taskFutures.add( + executor.submit( + () -> { + processSingleTable( + datasetId, tableName, collectedResults, resultSchemaFields, processor); + return null; + })); + } + + try { + waitForTasksCompletion(taskFutures); + } catch (ExecutionException e) { + throw new SQLException("Error while fetching metadata", e); + } + } } diff --git a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetPrimaryKeys.sql b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetPrimaryKeys.sql deleted file mode 100644 index 282910fb9721..000000000000 --- a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetPrimaryKeys.sql +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -SELECT table_catalog AS TABLE_CAT, - table_schema AS TABLE_SCHEM, - table_name AS TABLE_NAME, - column_name AS COLUMN_NAME, - ordinal_position AS KEY_SEQ, - constraint_name AS PK_NAME -FROM - %s.%s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE -WHERE - table_name = '%s' - AND CONTAINS_SUBSTR(constraint_name - , 'pk$') -ORDER BY - COLUMN_NAME; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java index 2b72ed01b8a3..69ab58fe9faa 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java @@ -159,24 +159,6 @@ public void testBigqueryDatabaseMetaDataGetters() throws SQLException { assertEquals("Project", dbMetadata.getCatalogTerm()); } - @Test - public void testReadSqlFromFile() throws SQLException { - BigQueryDatabaseMetaData dbMetadata = new BigQueryDatabaseMetaData(bigQueryConnection); - - String primaryKeysQuery = - BigQueryDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql"); - assertTrue(primaryKeysQuery.contains("pk$")); - - try { - when(bigQueryConnection.prepareStatement(primaryKeysQuery)).thenCallRealMethod(); - String sql = - dbMetadata.replaceSqlParameters( - primaryKeysQuery, "project_name", "dataset_name", "table_name"); - assertTrue(sql.contains("project_name.dataset_name.INFORMATION_SCHEMA.KEY_COLUMN_USAGE")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } @Test public void testNeedsListing() { @@ -3251,35 +3233,6 @@ public void testWrapperMethods() throws SQLException { assertThat((Throwable) e).hasMessageThat().contains("Cannot unwrap to java.sql.Connection"); } - @Test - public void testMetadataMethodsDoNotInterfere() throws SQLException { - Statement mockStatement1 = mock(Statement.class); - Statement mockStatement2 = mock(Statement.class); - ResultSet mockResultSet1 = mock(ResultSet.class); - ResultSet mockResultSet2 = mock(ResultSet.class); - - when(bigQueryConnection.createStatement()) - .thenReturn(mockStatement1) - .thenReturn(mockStatement2); - - when(mockStatement1.executeQuery(any())).thenReturn(mockResultSet1); - when(mockStatement2.executeQuery(any())).thenReturn(mockResultSet2); - - // Call first metadata method - ResultSet rs1 = dbMetadata.getPrimaryKeys("cat", "schema", "table"); - assertSame(mockResultSet1, rs1); - - // Call second metadata method - ResultSet rs2 = dbMetadata.getImportedKeys("cat", "schema", "table"); - assertSame(mockResultSet2, rs2); - - // Verify closeOnCompletion was called on both statements - verify(mockStatement1).closeOnCompletion(); - verify(mockStatement2).closeOnCompletion(); - - // Verify connection.createStatement() was called twice - verify(bigQueryConnection, times(2)).createStatement(); - } @ParameterizedTest @EnumSource(StandardSQLTypeName.class) From be0fd220ae30b624ee8a7fbd9fc3f19ffcedae97 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 7 Jul 2026 23:43:56 +0000 Subject: [PATCH 2/8] add sqlexception handling and proper cancellation --- .../jdbc/BigQueryDatabaseMetaData.java | 30 +++++++++++-------- .../jdbc/BigQueryDatabaseMetaDataTest.java | 2 -- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 824f1bbb011b..0722d381d0e6 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -5078,8 +5078,13 @@ private void processSingleTable( FieldList resultSchemaFields, TableProcessor processor) throws SQLException { - Table bqTable = - bigquery.getTable(TableId.of(datasetId.getProject(), datasetId.getDataset(), tableName)); + Table bqTable; + try { + bqTable = + bigquery.getTable(TableId.of(datasetId.getProject(), datasetId.getDataset(), tableName)); + } catch (Exception e) { + throw new SQLException("Error while fetching table metadata: " + e.getMessage(), e); + } if (bqTable != null && bqTable.getDefinition() != null) { processor.process(bqTable, collectedResults, resultSchemaFields); } @@ -5101,20 +5106,21 @@ private void processTargetTablesConcurrently( ExecutorService executor = connection.getMetadataExecutor(); List> taskFutures = new ArrayList<>(); - for (DatasetId datasetId : targetDatasets) { - taskFutures.add( - executor.submit( - () -> { - processSingleTable( - datasetId, tableName, collectedResults, resultSchemaFields, processor); - return null; - })); - } - try { + for (DatasetId datasetId : targetDatasets) { + taskFutures.add( + executor.submit( + () -> { + processSingleTable( + datasetId, tableName, collectedResults, resultSchemaFields, processor); + return null; + })); + } waitForTasksCompletion(taskFutures); } catch (ExecutionException e) { throw new SQLException("Error while fetching metadata", e); + } finally { + taskFutures.forEach(future -> future.cancel(true)); } } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java index 69ab58fe9faa..f99ba0bbe5dd 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java @@ -159,7 +159,6 @@ public void testBigqueryDatabaseMetaDataGetters() throws SQLException { assertEquals("Project", dbMetadata.getCatalogTerm()); } - @Test public void testNeedsListing() { assertTrue(dbMetadata.needsListing(null), "Null pattern should require listing"); @@ -3233,7 +3232,6 @@ public void testWrapperMethods() throws SQLException { assertThat((Throwable) e).hasMessageThat().contains("Cannot unwrap to java.sql.Connection"); } - @ParameterizedTest @EnumSource(StandardSQLTypeName.class) public void testMetadataAndResultSetMetadataTypeMappingConsistency(StandardSQLTypeName type) { From f7af9a22bc99834cb9208b6478f461128eff02a7 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 7 Jul 2026 23:56:51 +0000 Subject: [PATCH 3/8] re-use helper methods and preserve exception details --- .../jdbc/BigQueryDatabaseMetaData.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 0722d381d0e6..0209cce6d5cd 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -2466,7 +2466,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); } - Schema defineGetPrimaryKeysSchema() { + private Schema defineGetPrimaryKeysSchema() { List fields = new ArrayList<>(6); fields.add( Field.newBuilder("TABLE_CAT", StandardSQLTypeName.STRING) @@ -2495,7 +2495,7 @@ Schema defineGetPrimaryKeysSchema() { return Schema.of(fields); } - void processPrimaryKey( + private void processPrimaryKey( TableConstraints constraints, TableId tableId, List collectedResults, @@ -2505,20 +2505,18 @@ void processPrimaryKey( List pkColumns = pk.getColumns(); for (int i = 0; i < pkColumns.size(); i++) { List row = new ArrayList<>(6); - row.add( - FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getProject())); // 1. TABLE_CAT - row.add( - FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getDataset())); // 2. TABLE_SCHEM - row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, tableId.getTable())); // 3. TABLE_NAME - row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, pkColumns.get(i))); // 4. COLUMN_NAME - row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, String.valueOf(i + 1))); // 5. KEY_SEQ - row.add(FieldValue.of(FieldValue.Attribute.PRIMITIVE, null)); // 6. PK_NAME + row.add(createStringFieldValue(tableId.getProject())); // 1. TABLE_CAT + row.add(createStringFieldValue(tableId.getDataset())); // 2. TABLE_SCHEM + row.add(createStringFieldValue(tableId.getTable())); // 3. TABLE_NAME + row.add(createStringFieldValue(pkColumns.get(i))); // 4. COLUMN_NAME + row.add(createLongFieldValue((long) (i + 1))); // 5. KEY_SEQ + row.add(createNullFieldValue()); // 6. PK_NAME collectedResults.add(FieldValueList.of(row, resultSchemaFields)); } } } - Comparator defineGetPrimaryKeysComparator(FieldList resultSchemaFields) { + private Comparator defineGetPrimaryKeysComparator(FieldList resultSchemaFields) { final int COLUMN_NAME_IDX = resultSchemaFields.getIndex("COLUMN_NAME"); return Comparator.comparing( (FieldValueList fvl) -> getStringValueOrNull(fvl, COLUMN_NAME_IDX), @@ -5117,7 +5115,14 @@ private void processTargetTablesConcurrently( })); } waitForTasksCompletion(taskFutures); + if (Thread.currentThread().isInterrupted()) { + throw new SQLException("Interrupted while parallel-fetching metadata"); + } } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof SQLException) { + throw (SQLException) cause; + } throw new SQLException("Error while fetching metadata", e); } finally { taskFutures.forEach(future -> future.cancel(true)); From 621e53a27c7082af3fab3bc6aa204a813ffcfe43 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 7 Jul 2026 23:57:11 +0000 Subject: [PATCH 4/8] lint --- .../cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 0209cce6d5cd..30ef4a40bcd6 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -2507,10 +2507,10 @@ private void processPrimaryKey( List row = new ArrayList<>(6); row.add(createStringFieldValue(tableId.getProject())); // 1. TABLE_CAT row.add(createStringFieldValue(tableId.getDataset())); // 2. TABLE_SCHEM - row.add(createStringFieldValue(tableId.getTable())); // 3. TABLE_NAME - row.add(createStringFieldValue(pkColumns.get(i))); // 4. COLUMN_NAME - row.add(createLongFieldValue((long) (i + 1))); // 5. KEY_SEQ - row.add(createNullFieldValue()); // 6. PK_NAME + row.add(createStringFieldValue(tableId.getTable())); // 3. TABLE_NAME + row.add(createStringFieldValue(pkColumns.get(i))); // 4. COLUMN_NAME + row.add(createLongFieldValue((long) (i + 1))); // 5. KEY_SEQ + row.add(createNullFieldValue()); // 6. PK_NAME collectedResults.add(FieldValueList.of(row, resultSchemaFields)); } } From d1a914fa0561a7e8dfd2c84cf4346f0d707e1c23 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 8 Jul 2026 00:44:40 +0000 Subject: [PATCH 5/8] feat(bigquery-jdbc): Migrate `getImportedKeys` and `getCrossReference` to BQ API --- .../jdbc/BigQueryDatabaseMetaData.java | 210 +++++++++++++++--- .../DatabaseMetaData_GetCrossReference.sql | 72 ------ .../jdbc/DatabaseMetaData_GetImportedKeys.sql | 71 ------ 3 files changed, 181 insertions(+), 172 deletions(-) delete mode 100644 java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetCrossReference.sql delete mode 100644 java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetImportedKeys.sql diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 30ef4a40bcd6..faa282389e0b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -27,6 +27,7 @@ import com.google.cloud.bigquery.BigQuery.TableListOption; import com.google.cloud.bigquery.BigQuery.TableOption; import com.google.cloud.bigquery.BigQueryException; +import com.google.cloud.bigquery.ColumnReference; import com.google.cloud.bigquery.Dataset; import com.google.cloud.bigquery.DatasetId; import com.google.cloud.bigquery.Field; @@ -34,6 +35,7 @@ import com.google.cloud.bigquery.FieldList; import com.google.cloud.bigquery.FieldValue; import com.google.cloud.bigquery.FieldValueList; +import com.google.cloud.bigquery.ForeignKey; import com.google.cloud.bigquery.PrimaryKey; import com.google.cloud.bigquery.Routine; import com.google.cloud.bigquery.RoutineArgument; @@ -98,10 +100,7 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData { private static final String SCHEMA_TERM = "Dataset"; private static final String CATALOG_TERM = "Project"; private static final String PROCEDURE_TERM = "Procedure"; - private static final String GET_PRIMARY_KEYS_SQL = "DatabaseMetaData_GetPrimaryKeys.sql"; - private static final String GET_IMPORTED_KEYS_SQL = "DatabaseMetaData_GetImportedKeys.sql"; private static final String GET_EXPORTED_KEYS_SQL = "DatabaseMetaData_GetExportedKeys.sql"; - private static final String GET_CROSS_REFERENCE_SQL = "DatabaseMetaData_GetCrossReference.sql"; private static final int DEFAULT_PAGE_SIZE = 500; private static final int DEFAULT_QUEUE_CAPACITY = 5000; // Declared package-private for testing. @@ -2526,16 +2525,47 @@ private Comparator defineGetPrimaryKeysComparator(FieldList resu @Override public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { - String sql = readSqlFromFile(GET_IMPORTED_KEYS_SQL); - Statement stmt = this.connection.createStatement(); - try { - stmt.closeOnCompletion(); - String formattedSql = replaceSqlParameters(sql, catalog, schema, table); - return stmt.executeQuery(formattedSql); - } catch (SQLException e) { - closeStatementIgnoreException(stmt); - throw new BigQueryJdbcException("Error executing getImportedKeys", e); + if ((catalog != null && catalog.isEmpty()) + || (schema != null && schema.isEmpty()) + || table == null + || table.isEmpty()) { + LOG.warning( + "Returning empty ResultSet as one or more patterns are empty or catalog is empty."); + return new BigQueryJsonResultSet(); } + + final Schema resultSchema = defineForeignKeyResultSetSchema(); + final FieldList resultSchemaFields = resultSchema.getFields(); + + final List collectedResults = Collections.synchronizedList(new ArrayList<>()); + List targetDatasets = getTargetDatasets(catalog, schema); + + processTargetTablesConcurrently( + targetDatasets, + table, + collectedResults, + resultSchemaFields, + (bqTable, results, fields) -> { + TableConstraints constraints = null; + if (bqTable.getDefinition() instanceof StandardTableDefinition) { + constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); + } + if (constraints != null && constraints.getForeignKeys() != null) { + for (ForeignKey fk : constraints.getForeignKeys()) { + TableId pkTableId = fk.getReferencedTable(); + processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); + } + } + }); + + Comparator comparator = definePkTableSortComparator(resultSchemaFields); + sortResults(collectedResults, comparator, "getImportedKeys", LOG); + + final BlockingQueue queue = + new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + populateQueue(collectedResults, queue, resultSchemaFields); + signalEndOfData(queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); } @Override @@ -2562,24 +2592,55 @@ public ResultSet getCrossReference( String foreignSchema, String foreignTable) throws SQLException { - String sql = readSqlFromFile(GET_CROSS_REFERENCE_SQL); - Statement stmt = this.connection.createStatement(); - try { - stmt.closeOnCompletion(); - String formattedSql = - replaceSqlParameters( - sql, - parentCatalog, - parentSchema, - parentTable, - foreignCatalog, - foreignSchema, - foreignTable); - return stmt.executeQuery(formattedSql); - } catch (SQLException e) { - closeStatementIgnoreException(stmt); - throw new BigQueryJdbcException("Error executing getCrossReference", e); + if ((parentCatalog != null && parentCatalog.isEmpty()) + || (parentSchema != null && parentSchema.isEmpty()) + || parentTable == null + || parentTable.isEmpty() + || (foreignCatalog != null && foreignCatalog.isEmpty()) + || (foreignSchema != null && foreignSchema.isEmpty()) + || foreignTable == null + || foreignTable.isEmpty()) { + LOG.warning( + "Returning empty ResultSet as one or more patterns are empty or catalog is empty."); + return new BigQueryJsonResultSet(); } + + final Schema resultSchema = defineForeignKeyResultSetSchema(); + final FieldList resultSchemaFields = resultSchema.getFields(); + + final List collectedResults = Collections.synchronizedList(new ArrayList<>()); + List targetDatasets = getTargetDatasets(foreignCatalog, foreignSchema); + + processTargetTablesConcurrently( + targetDatasets, + foreignTable, + collectedResults, + resultSchemaFields, + (bqTable, results, fields) -> { + TableConstraints constraints = null; + if (bqTable.getDefinition() instanceof StandardTableDefinition) { + constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); + } + if (constraints != null && constraints.getForeignKeys() != null) { + for (ForeignKey fk : constraints.getForeignKeys()) { + TableId pkTableId = fk.getReferencedTable(); + if (matchesOrNullWildcard(parentCatalog, pkTableId.getProject()) + && matchesOrNullWildcard(parentSchema, pkTableId.getDataset()) + && parentTable.equals(pkTableId.getTable())) { + processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); + } + } + } + }); + + Comparator comparator = defineFkTableSortComparator(resultSchemaFields); + sortResults(collectedResults, comparator, "getCrossReference", LOG); + + final BlockingQueue queue = + new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + populateQueue(collectedResults, queue, resultSchemaFields); + signalEndOfData(queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); } @Override @@ -5001,6 +5062,10 @@ private List getAccessibleCatalogNames() throws SQLException { return sortedCatalogs; } + private boolean matchesOrNullWildcard(String expected, String actual) { + return expected == null || expected.equals(actual); + } + static String readSqlFromFile(String filename) { InputStream in; in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename); @@ -5128,4 +5193,91 @@ private void processTargetTablesConcurrently( taskFutures.forEach(future -> future.cancel(true)); } } + + private Schema defineForeignKeyResultSetSchema() { + return Schema.of( + Field.of("PKTABLE_CAT", StandardSQLTypeName.STRING), + Field.of("PKTABLE_SCHEM", StandardSQLTypeName.STRING), + Field.of("PKTABLE_NAME", StandardSQLTypeName.STRING), + Field.of("PKCOLUMN_NAME", StandardSQLTypeName.STRING), + Field.of("FKTABLE_CAT", StandardSQLTypeName.STRING), + Field.of("FKTABLE_SCHEM", StandardSQLTypeName.STRING), + Field.of("FKTABLE_NAME", StandardSQLTypeName.STRING), + Field.of("FKCOLUMN_NAME", StandardSQLTypeName.STRING), + Field.of("KEY_SEQ", StandardSQLTypeName.INT64), + Field.of("UPDATE_RULE", StandardSQLTypeName.INT64), + Field.of("DELETE_RULE", StandardSQLTypeName.INT64), + Field.of("FK_NAME", StandardSQLTypeName.STRING), + Field.of("PK_NAME", StandardSQLTypeName.STRING), + Field.of("DEFERRABILITY", StandardSQLTypeName.INT64)); + } + + private void processForeignKey( + ForeignKey fk, + TableId pkTableId, + TableId fkTableId, + List collectedResults, + FieldList resultSchemaFields) { + List colRefs = fk.getColumnReferences(); + if (colRefs != null) { + for (int i = 0; i < colRefs.size(); i++) { + ColumnReference colRef = colRefs.get(i); + List row = new ArrayList<>(); + row.add(createStringFieldValue(pkTableId.getProject())); // PKTABLE_CAT + row.add(createStringFieldValue(pkTableId.getDataset())); // PKTABLE_SCHEM + row.add(createStringFieldValue(pkTableId.getTable())); // PKTABLE_NAME + row.add(createStringFieldValue(colRef.getReferencedColumn())); // PKCOLUMN_NAME + row.add(createStringFieldValue(fkTableId.getProject())); // FKTABLE_CAT + row.add(createStringFieldValue(fkTableId.getDataset())); // FKTABLE_SCHEM + row.add(createStringFieldValue(fkTableId.getTable())); // FKTABLE_NAME + row.add(createStringFieldValue(colRef.getReferencingColumn())); // FKCOLUMN_NAME + row.add(createLongFieldValue((long) (i + 1))); // KEY_SEQ + row.add(createNullFieldValue()); // UPDATE_RULE + row.add(createNullFieldValue()); // DELETE_RULE + row.add(createStringFieldValue(fk.getName())); // FK_NAME + row.add(createNullFieldValue()); // PK_NAME + row.add(createNullFieldValue()); // DEFERRABILITY + + collectedResults.add(FieldValueList.of(row, resultSchemaFields)); + } + } + } + + private Comparator definePkTableSortComparator(FieldList resultSchemaFields) { + final int PKTABLE_CAT_IDX = resultSchemaFields.getIndex("PKTABLE_CAT"); + final int PKTABLE_SCHEM_IDX = resultSchemaFields.getIndex("PKTABLE_SCHEM"); + final int PKTABLE_NAME_IDX = resultSchemaFields.getIndex("PKTABLE_NAME"); + final int KEY_SEQ_IDX = resultSchemaFields.getIndex("KEY_SEQ"); + return Comparator.comparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, PKTABLE_CAT_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, PKTABLE_SCHEM_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, PKTABLE_NAME_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getLongValueOrNull(fvl, KEY_SEQ_IDX), + Comparator.nullsFirst(Long::compareTo)); + } + + private Comparator defineFkTableSortComparator(FieldList resultSchemaFields) { + final int FKTABLE_CAT_IDX = resultSchemaFields.getIndex("FKTABLE_CAT"); + final int FKTABLE_SCHEM_IDX = resultSchemaFields.getIndex("FKTABLE_SCHEM"); + final int FKTABLE_NAME_IDX = resultSchemaFields.getIndex("FKTABLE_NAME"); + final int KEY_SEQ_IDX = resultSchemaFields.getIndex("KEY_SEQ"); + return Comparator.comparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, FKTABLE_CAT_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, FKTABLE_SCHEM_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getStringValueOrNull(fvl, FKTABLE_NAME_IDX), + Comparator.nullsFirst(String::compareTo)) + .thenComparing( + (FieldValueList fvl) -> getLongValueOrNull(fvl, KEY_SEQ_IDX), + Comparator.nullsFirst(Long::compareTo)); + } } diff --git a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetCrossReference.sql b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetCrossReference.sql deleted file mode 100644 index da8386270457..000000000000 --- a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetCrossReference.sql +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -SELECT PKTABLE_CAT, - PKTABLE_SCHEM, - PKTABLE_NAME, - PRIMARY.column_name AS PKCOLUMN_NAME, - FOREIGN.constraint_catalog AS FKTABLE_CAT, - FOREIGN.constraint_schema AS FKTABLE_SCHEM, - FOREIGN.table_name AS FKTABLE_NAME, - FOREIGN.column_name AS FKCOLUMN_NAME, - FOREIGN.ordinal_position AS KEY_SEQ, - NULL AS UPDATE_RULE, - NULL AS DELETE_RULE, - FOREIGN.constraint_name AS FK_NAME, - PRIMARY.constraint_name AS PK_NAME, - NULL AS DEFERRABILITY -FROM (SELECT DISTINCT CCU.table_catalog AS PKTABLE_CAT, - CCU.table_schema AS PKTABLE_SCHEM, - CCU.table_name AS PKTABLE_NAME, - TC.constraint_catalog, - TC.constraint_schema, - TC.constraint_name, - TC.table_catalog, - TC.table_schema, - TC.table_name, - TC.constraint_type, - KCU.column_name, - KCU.ordinal_position, - KCU.position_in_unique_constraint - FROM `%1$s.%2$s.INFORMATION_SCHEMA.TABLE_CONSTRAINTS` TC - INNER JOIN - `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` KCU - USING - (constraint_catalog, - constraint_schema, - constraint_name, - table_catalog, - table_schema, - table_name) - INNER JOIN - `%1$s.%2$s.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE` CCU - USING - (constraint_catalog, - constraint_schema, - constraint_name) - WHERE constraint_type = 'FOREIGN KEY' - AND TC.table_name = '%6$s') FOREIGN - INNER JOIN (SELECT * - FROM `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` - WHERE position_in_unique_constraint IS NULL - AND RTRIM(table_name) = '%3$s') PRIMARY -ON - FOREIGN.PKTABLE_CAT = PRIMARY.table_catalog - AND FOREIGN.PKTABLE_SCHEM = PRIMARY.table_schema - AND FOREIGN.PKTABLE_NAME = PRIMARY.table_name - AND FOREIGN.position_in_unique_constraint = - PRIMARY.ordinal_position -ORDER BY FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, KEY_SEQ \ No newline at end of file diff --git a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetImportedKeys.sql b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetImportedKeys.sql deleted file mode 100644 index 3f4142eb051f..000000000000 --- a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetImportedKeys.sql +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -SELECT PKTABLE_CAT, - PKTABLE_SCHEM, - PKTABLE_NAME, - PRIMARY.column_name AS PKCOLUMN_NAME, - FOREIGN.constraint_catalog AS FKTABLE_CAT, - FOREIGN.constraint_schema AS FKTABLE_SCHEM, - FOREIGN.table_name AS FKTABLE_NAME, - FOREIGN.column_name AS FKCOLUMN_NAME, - FOREIGN.ordinal_position AS KEY_SEQ, - NULL AS UPDATE_RULE, - NULL AS DELETE_RULE, - FOREIGN.constraint_name AS FK_NAME, - PRIMARY.constraint_name AS PK_NAME, - NULL AS DEFERRABILITY -FROM (SELECT DISTINCT CCU.table_catalog AS PKTABLE_CAT, - CCU.table_schema AS PKTABLE_SCHEM, - CCU.table_name AS PKTABLE_NAME, - TC.constraint_catalog, - TC.constraint_schema, - TC.constraint_name, - TC.table_catalog, - TC.table_schema, - TC.table_name, - TC.constraint_type, - KCU.column_name, - KCU.ordinal_position, - KCU.position_in_unique_constraint - FROM `%1$s.%2$s.INFORMATION_SCHEMA.TABLE_CONSTRAINTS` TC - INNER JOIN - `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` KCU - USING - (constraint_catalog, - constraint_schema, - constraint_name, - table_catalog, - table_schema, - table_name) - INNER JOIN - `%1$s.%2$s.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE` CCU - USING - (constraint_catalog, - constraint_schema, - constraint_name) - WHERE constraint_type = 'FOREIGN KEY' - AND TC.table_name = '%3$s') FOREIGN - INNER JOIN (SELECT * - FROM `%1$s.%2$s.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` - WHERE position_in_unique_constraint IS NULL) PRIMARY -ON - FOREIGN.PKTABLE_CAT = PRIMARY.table_catalog - AND FOREIGN.PKTABLE_SCHEM = PRIMARY.table_schema - AND FOREIGN.PKTABLE_NAME = PRIMARY.table_name - AND FOREIGN.position_in_unique_constraint = - PRIMARY.ordinal_position -ORDER BY PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, KEY_SEQ \ No newline at end of file From c569ed51c392231542553383f13880ffa8bdd2ba Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 8 Jul 2026 00:56:15 +0000 Subject: [PATCH 6/8] simplify bqTable.getTableConstraints() call --- .../bigquery/jdbc/BigQueryDatabaseMetaData.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index faa282389e0b..e0e73c78efd1 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -45,7 +45,6 @@ import com.google.cloud.bigquery.StandardSQLField; import com.google.cloud.bigquery.StandardSQLTableType; import com.google.cloud.bigquery.StandardSQLTypeName; -import com.google.cloud.bigquery.StandardTableDefinition; import com.google.cloud.bigquery.Table; import com.google.cloud.bigquery.TableConstraints; import com.google.cloud.bigquery.TableDefinition; @@ -2448,10 +2447,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr collectedResults, resultSchemaFields, (bqTable, results, fields) -> { - TableConstraints constraints = null; - if (bqTable.getDefinition() instanceof StandardTableDefinition) { - constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); - } + TableConstraints constraints = bqTable.getTableConstraints(); processPrimaryKey(constraints, bqTable.getTableId(), results, fields); }); @@ -2546,10 +2542,7 @@ public ResultSet getImportedKeys(String catalog, String schema, String table) collectedResults, resultSchemaFields, (bqTable, results, fields) -> { - TableConstraints constraints = null; - if (bqTable.getDefinition() instanceof StandardTableDefinition) { - constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); - } + TableConstraints constraints = bqTable.getTableConstraints(); if (constraints != null && constraints.getForeignKeys() != null) { for (ForeignKey fk : constraints.getForeignKeys()) { TableId pkTableId = fk.getReferencedTable(); @@ -2617,10 +2610,7 @@ public ResultSet getCrossReference( collectedResults, resultSchemaFields, (bqTable, results, fields) -> { - TableConstraints constraints = null; - if (bqTable.getDefinition() instanceof StandardTableDefinition) { - constraints = ((StandardTableDefinition) bqTable.getDefinition()).getTableConstraints(); - } + TableConstraints constraints = bqTable.getTableConstraints(); if (constraints != null && constraints.getForeignKeys() != null) { for (ForeignKey fk : constraints.getForeignKeys()) { TableId pkTableId = fk.getReferencedTable(); From 751316360ad8025b79b77eabe109902828b147ae Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 8 Jul 2026 20:51:40 +0000 Subject: [PATCH 7/8] add null safety check --- .../jdbc/BigQueryDatabaseMetaData.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 4709ae85f282..75414771600c 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -45,7 +45,6 @@ import com.google.cloud.bigquery.StandardSQLField; import com.google.cloud.bigquery.StandardSQLTableType; import com.google.cloud.bigquery.StandardSQLTypeName; -import com.google.cloud.bigquery.StandardTableDefinition; import com.google.cloud.bigquery.Table; import com.google.cloud.bigquery.TableConstraints; import com.google.cloud.bigquery.TableDefinition; @@ -2530,7 +2529,7 @@ public ResultSet getImportedKeys(String catalog, String schema, String table) || table == null || table.isEmpty()) { LOG.warning( - "Returning empty ResultSet as one or more patterns are empty or catalog is empty."); + "Returning empty ResultSet as required parameters are null/empty, or catalog/schema parameters are empty."); return new BigQueryJsonResultSet(); } @@ -2600,7 +2599,7 @@ public ResultSet getCrossReference( || foreignTable == null || foreignTable.isEmpty()) { LOG.warning( - "Returning empty ResultSet as one or more patterns are empty or catalog is empty."); + "Returning empty ResultSet as required parameters are null/empty, or catalog/schema parameters are empty."); return new BigQueryJsonResultSet(); } @@ -2622,8 +2621,8 @@ public ResultSet getCrossReference( if (constraints != null && constraints.getForeignKeys() != null) { for (ForeignKey fk : constraints.getForeignKeys()) { TableId pkTableId = fk.getReferencedTable(); - if (matchesOrNullWildcard(parentCatalog, pkTableId.getProject()) - && matchesOrNullWildcard(parentSchema, pkTableId.getDataset()) + if (equalsOrNullMatchesAll(parentCatalog, pkTableId.getProject()) + && equalsOrNullMatchesAll(parentSchema, pkTableId.getDataset()) && parentTable.equals(pkTableId.getTable())) { processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); } @@ -5060,7 +5059,7 @@ private List getAccessibleCatalogNames() throws SQLException { return sortedCatalogs; } - private boolean matchesOrNullWildcard(String expected, String actual) { + private boolean equalsOrNullMatchesAll(String expected, String actual) { return expected == null || expected.equals(actual); } @@ -5212,6 +5211,7 @@ private void processTargetTablesConcurrently( taskFutures.forEach(future -> future.cancel(true)); } } + private Schema defineForeignKeyResultSetSchema() { return Schema.of( Field.of("PKTABLE_CAT", StandardSQLTypeName.STRING), @@ -5236,6 +5236,13 @@ private void processForeignKey( TableId fkTableId, List collectedResults, FieldList resultSchemaFields) { + if (pkTableId == null) { + LOG.warning( + String.format( + "Skipping foreign key '%s' on table '%s' because its referenced table ID is null.", + fk.getName() != null ? fk.getName() : "unnamed", fkTableId.getTable())); + return; + } List colRefs = fk.getColumnReferences(); if (colRefs != null) { for (int i = 0; i < colRefs.size(); i++) { From 9dad6e19b950ad71d29342f9fcc8185cebf343d9 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 8 Jul 2026 23:06:42 +0000 Subject: [PATCH 8/8] nit: early return --- .../jdbc/BigQueryDatabaseMetaData.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 75414771600c..c2ab4e331ef5 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -2548,11 +2548,12 @@ public ResultSet getImportedKeys(String catalog, String schema, String table) ignoreAccessErrors, (bqTable, results, fields) -> { TableConstraints constraints = bqTable.getTableConstraints(); - if (constraints != null && constraints.getForeignKeys() != null) { - for (ForeignKey fk : constraints.getForeignKeys()) { - TableId pkTableId = fk.getReferencedTable(); - processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); - } + if (constraints == null || constraints.getForeignKeys() == null) { + return; + } + for (ForeignKey fk : constraints.getForeignKeys()) { + TableId pkTableId = fk.getReferencedTable(); + processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); } }); @@ -2618,15 +2619,17 @@ public ResultSet getCrossReference( ignoreAccessErrors, (bqTable, results, fields) -> { TableConstraints constraints = bqTable.getTableConstraints(); - if (constraints != null && constraints.getForeignKeys() != null) { - for (ForeignKey fk : constraints.getForeignKeys()) { - TableId pkTableId = fk.getReferencedTable(); - if (equalsOrNullMatchesAll(parentCatalog, pkTableId.getProject()) - && equalsOrNullMatchesAll(parentSchema, pkTableId.getDataset()) - && parentTable.equals(pkTableId.getTable())) { - processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); - } + if (constraints == null || constraints.getForeignKeys() == null) { + return; + } + for (ForeignKey fk : constraints.getForeignKeys()) { + TableId pkTableId = fk.getReferencedTable(); + if (!equalsOrNullMatchesAll(parentCatalog, pkTableId.getProject()) + || !equalsOrNullMatchesAll(parentSchema, pkTableId.getDataset()) + || !parentTable.equals(pkTableId.getTable())) { + continue; } + processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); } });