From 0349c8eba03f1284a1e435c07cb48ba136fb3a96 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 9 Jul 2026 15:46:56 +0200 Subject: [PATCH 1/2] feat(bigquery-jdbc): Migrate `getImportedKeys` and `getCrossReference` to BQ API (#13692) b/532249777 This PR migrates the `getImportedKeys` and `getCrossReference` JDBC metadata methods in `BigQueryDatabaseMetaData` to retrieve constraints natively via the Google Cloud Java Client API instead of running legacy raw SQL files on BigQuery. ## Changes ### **API-based Implementations** - Migrated `getImportedKeys(...)` to fetch and scan foreign keys from standard table definitions. - Migrated `getCrossReference(...)` to matching parent and foreign table references using client-side definitions. - Utilized the shared parallel table processor utilities (`processTargetTablesConcurrently(...)` and `processSingleTable(...)`) to run dataset scans concurrently. ### **Sorting & Correctness** - Separated sorting behavior according to the JDBC specification: - `getImportedKeys` is sorted by parent table: `PKTABLE_CAT, PKTABLE_SCHEM, PKTABLE_NAME, KEY_SEQ`. Introduced `definePkTableSortComparator` for this. - `getCrossReference` (and future `getExportedKeys`) is sorted by child table: `FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, KEY_SEQ`. Uses `defineFkTableSortComparator`. - Introduced a private helper `matchesOrNullWildcard(...)` to simplify and clean up catalog/schema bounds comparisons. --- .../jdbc/BigQueryDatabaseMetaData.java | 399 +++++++++++++----- .../DatabaseMetaData_GetCrossReference.sql | 72 ---- .../jdbc/DatabaseMetaData_GetExportedKeys.sql | 71 ---- .../jdbc/DatabaseMetaData_GetImportedKeys.sql | 71 ---- .../jdbc/BigQueryDatabaseMetaDataTest.java | 345 ++++++++++++--- .../jdbc/it/ITDatabaseMetadataTest.java | 58 +++ 6 files changed, 643 insertions(+), 373 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_GetExportedKeys.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 210c379e8373..e6495b758f51 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; @@ -43,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; @@ -51,15 +52,11 @@ import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.jdbc.BigQueryJdbcTypeMappings.ColumnTypeInfo; import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.RowIdLifetime; import java.sql.SQLException; -import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; @@ -67,7 +64,6 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Scanner; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; @@ -88,20 +84,14 @@ * * @see BigQueryStatement */ -// TODO(neenu): test and verify after post MVP implementation. class BigQueryDatabaseMetaData implements DatabaseMetaData { final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); private static final String DATABASE_PRODUCT_NAME = "Google BigQuery"; private static final String DATABASE_PRODUCT_VERSION = "2.0"; private static final String DRIVER_NAME = "GoogleJDBCDriverForGoogleBigQuery"; - 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. @@ -1822,11 +1812,9 @@ public ResultSet getCatalogs() throws SQLException { final BlockingQueue queue = new LinkedBlockingQueue<>(catalogRows.isEmpty() ? 1 : catalogRows.size() + 1); - populateQueue(catalogRows, queue, schemaFields); - signalEndOfData(queue, schemaFields); + Future fetcherFuture = populateQueueAsync(catalogRows, queue, schemaFields); - return BigQueryJsonResultSet.of( - catalogsSchema, catalogRows.size(), queue, null, new Future[0]); + return BigQueryJsonResultSet.of(catalogsSchema, catalogRows.size(), queue, null, fetcherFuture); } Schema defineGetCatalogsSchema() { @@ -1854,11 +1842,11 @@ public ResultSet getTableTypes() { BlockingQueue queue = new LinkedBlockingQueue<>(tableTypeRows.size() + 1); - populateQueue(tableTypeRows, queue, tableTypesSchema.getFields()); - signalEndOfData(queue, tableTypesSchema.getFields()); + Future fetcherFuture = + populateQueueAsync(tableTypeRows, queue, tableTypesSchema.getFields()); return BigQueryJsonResultSet.of( - tableTypesSchema, tableTypeRows.size(), queue, null, new Future[0]); + tableTypesSchema, tableTypeRows.size(), queue, null, fetcherFuture); } static Schema defineGetTableTypesSchema() { @@ -2415,17 +2403,6 @@ Schema defineGetVersionColumnsSchema() { return Schema.of(fields); } - private void closeStatementIgnoreException(Statement statement) { - if (statement == null) { - return; - } - try { - statement.close(); - } catch (SQLException e) { - // pass - } - } - @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { if ((catalog != null && catalog.isEmpty()) @@ -2451,10 +2428,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr resultSchemaFields, ignoreAccessErrors, (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); }); @@ -2463,9 +2437,8 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr final BlockingQueue queue = new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); - populateQueue(collectedResults, queue, resultSchemaFields); - signalEndOfData(queue, resultSchemaFields); - return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); + Future fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture); } private Schema defineGetPrimaryKeysSchema() { @@ -2529,31 +2502,97 @@ 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 required parameters are null/empty, or catalog/schema parameters are 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); + + boolean ignoreAccessErrors = (catalog == null); + processTargetTablesConcurrently( + targetDatasets, + table, + collectedResults, + resultSchemaFields, + ignoreAccessErrors, + (bqTable, results, fields) -> { + TableConstraints constraints = bqTable.getTableConstraints(); + if (constraints == null || constraints.getForeignKeys() == null) { + return; + } + 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); + Future fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture); } @Override public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { - String sql = readSqlFromFile(GET_EXPORTED_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 getExportedKeys", e); + if ((catalog != null && catalog.isEmpty()) + || (schema != null && schema.isEmpty()) + || table == null + || table.isEmpty()) { + LOG.warning( + "Returning empty ResultSet as required parameters are null/empty, or catalog/schema parameters are empty."); + return new BigQueryJsonResultSet(); } + + final Schema resultSchema = defineForeignKeyResultSetSchema(); + final FieldList resultSchemaFields = resultSchema.getFields(); + + final List collectedResults = Collections.synchronizedList(new ArrayList<>()); + List targetDatasets = getTargetDatasets(catalog, null); + + boolean ignoreAccessErrors = (catalog == null); + processTargetTablesConcurrently( + targetDatasets, + null, + collectedResults, + resultSchemaFields, + ignoreAccessErrors, + (bqTable, results, fields) -> { + TableConstraints constraints = bqTable.getTableConstraints(); + if (constraints == null || constraints.getForeignKeys() == null) { + return; + } + for (ForeignKey fk : constraints.getForeignKeys()) { + TableId pkTableId = fk.getReferencedTable(); + if (pkTableId == null + || !equalsOrNullMatchesAll(catalog, pkTableId.getProject()) + || !equalsOrNullMatchesAll(schema, pkTableId.getDataset()) + || !table.equals(pkTableId.getTable())) { + continue; + } + processForeignKey(fk, pkTableId, bqTable.getTableId(), results, fields); + } + }); + + Comparator comparator = defineFkTableSortComparator(resultSchemaFields); + sortResults(collectedResults, comparator, "getExportedKeys", LOG); + + final BlockingQueue queue = + new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + Future fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture); } @Override @@ -2565,24 +2604,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 required parameters are null/empty, or catalog/schema parameters are 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); + + boolean ignoreAccessErrors = (foreignCatalog == null); + processTargetTablesConcurrently( + targetDatasets, + foreignTable, + collectedResults, + resultSchemaFields, + ignoreAccessErrors, + (bqTable, results, fields) -> { + TableConstraints constraints = bqTable.getTableConstraints(); + 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); + } + }); + + Comparator comparator = defineFkTableSortComparator(resultSchemaFields); + sortResults(collectedResults, comparator, "getCrossReference", LOG); + + final BlockingQueue queue = + new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY); + Future fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture); } @Override @@ -2598,10 +2668,9 @@ public ResultSet getTypeInfo() { final BlockingQueue queue = new LinkedBlockingQueue<>(typeInfoRows.size() + 1); - populateQueue(typeInfoRows, queue, schemaFields); - signalEndOfData(queue, schemaFields); + Future fetcherFuture = populateQueueAsync(typeInfoRows, queue, schemaFields); return BigQueryJsonResultSet.of( - typeInfoSchema, typeInfoRows.size(), queue, null, new Future[0]); + typeInfoSchema, typeInfoRows.size(), queue, null, fetcherFuture); } Schema defineGetTypeInfoSchema() { @@ -3522,9 +3591,8 @@ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLExce } Comparator comparator = defineGetSchemasComparator(resultSchemaFields); sortResults(collectedResults, comparator, "getSchemas", LOG); - populateQueue(collectedResults, queue, resultSchemaFields); - signalEndOfData(queue, resultSchemaFields); - return BigQueryJsonResultSet.of(resultSchema, -1, queue, null); + Future fetcherFuture = populateQueueAsync(collectedResults, queue, resultSchemaFields); + return BigQueryJsonResultSet.of(resultSchema, -1, queue, null, fetcherFuture); } // Multi-Catalog Path: fan out using connection-scoped metadataExecutor @@ -4836,6 +4904,19 @@ private void waitForTasksCompletion(List> taskFutures) throws Executio LOG.info("Finished waiting for tasks."); } + private Future populateQueueAsync( + List collectedResults, + BlockingQueue queue, + FieldList resultSchemaFields) { + return connection + .getMetadataExecutor() + .submit( + () -> { + populateQueue(collectedResults, queue, resultSchemaFields); + signalEndOfData(queue, resultSchemaFields); + }); + } + private void populateQueue( List collectedResults, BlockingQueue queue, @@ -5004,22 +5085,8 @@ private List getAccessibleCatalogNames() throws SQLException { return sortedCatalogs; } - static String readSqlFromFile(String filename) { - InputStream in; - in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename); - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - StringBuilder builder = new StringBuilder(); - try (Scanner scanner = new Scanner(reader)) { - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - builder.append(line).append("\n"); - } - } - return builder.toString(); - } - - String replaceSqlParameters(String sql, String... params) throws SQLException { - return String.format(sql, (Object[]) params); + private boolean equalsOrNullMatchesAll(String expected, String actual) { + return expected == null || expected.equals(actual); } private void writeErrorToQueue(BlockingQueue queue, Throwable t) { @@ -5109,7 +5176,7 @@ private void processTargetTablesConcurrently( boolean ignoreAccessErrors, TableProcessor processor) throws SQLException { - if (targetDatasets.size() == 1) { + if (targetDatasets.size() == 1 && tableName != null) { processSingleTable( targetDatasets.get(0), tableName, @@ -5124,19 +5191,59 @@ private void processTargetTablesConcurrently( List> taskFutures = new ArrayList<>(); try { + List> tasks = new ArrayList<>(); for (DatasetId datasetId : targetDatasets) { - taskFutures.add( - executor.submit( + if (tableName != null) { + tasks.add( + () -> { + processSingleTable( + datasetId, + tableName, + collectedResults, + resultSchemaFields, + ignoreAccessErrors, + processor); + return null; + }); + continue; + } + + try { + Page tablesPage = + bigquery.listTables(datasetId, TableListOption.pageSize(DEFAULT_PAGE_SIZE)); + if (tablesPage == null) { + continue; + } + for (Table table : tablesPage.iterateAll()) { + if (table.getDefinition() == null + || table.getDefinition().getType() != TableDefinition.Type.TABLE) { + continue; + } + tasks.add( () -> { processSingleTable( datasetId, - tableName, + table.getTableId().getTable(), collectedResults, resultSchemaFields, ignoreAccessErrors, processor); return null; - })); + }); + } + } catch (BigQueryException e) { + if (ignoreAccessErrors && (e.getCode() == 404 || e.getCode() == 403)) { + LOG.info( + "Dataset '%s' not found/accessible in project '%s' (API error %d). Skipping.", + datasetId.getDataset(), datasetId.getProject(), e.getCode()); + continue; + } + throw new SQLException("Error while listing tables: " + e.getMessage(), e); + } + } + + for (Callable task : tasks) { + taskFutures.add(executor.submit(task)); } waitForTasksCompletion(taskFutures); if (Thread.currentThread().isInterrupted()) { @@ -5152,4 +5259,98 @@ 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) { + 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++) { + 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_GetExportedKeys.sql b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetExportedKeys.sql deleted file mode 100644 index 4058f6bff60a..000000000000 --- a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetExportedKeys.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') 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 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 f99ba0bbe5dd..e0904eb7fdde 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 @@ -1011,7 +1011,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListWithPattern() throws Ex Page page = mock(Page.class); when(page.iterateAll()).thenReturn(Arrays.asList(proc1, func1, proc2, otherProc)); - when(bigqueryClient.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption.class))) + when(bigqueryClient.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class))) .thenReturn(page); Pattern regex = dbMetadata.compileSqlLikePattern(pattern); @@ -1030,7 +1030,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListWithPattern() throws Ex dbMetadata.LOG); verify(bigqueryClient, times(1)) - .listRoutines(eq(datasetId), any(BigQuery.RoutineListOption.class)); + .listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class)); verify(bigqueryClient, never()).getRoutine(any(RoutineId.class)); assertNotNull(results); @@ -1055,7 +1055,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListNoPattern() throws Exce Page page = mock(Page.class); when(page.iterateAll()).thenReturn(Arrays.asList(proc1, func1)); - when(bigqueryClient.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption.class))) + when(bigqueryClient.listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class))) .thenReturn(page); Pattern regex = dbMetadata.compileSqlLikePattern(pattern); @@ -1072,7 +1072,7 @@ public void testFindMatchingBigQueryObjects_Routines_ListNoPattern() throws Exce dbMetadata.LOG); verify(bigqueryClient, times(1)) - .listRoutines(eq(datasetId), any(BigQuery.RoutineListOption.class)); + .listRoutines(eq(datasetId), any(BigQuery.RoutineListOption[].class)); assertNotNull(results); List resultList = new ArrayList<>(results); @@ -1108,7 +1108,7 @@ public void testFindMatchingBigQueryObjects_Routines_GetSpecific() throws Except verify(bigqueryClient, times(1)).getRoutine(eq(routineId)); verify(bigqueryClient, never()) - .listRoutines(any(DatasetId.class), any(BigQuery.RoutineListOption.class)); + .listRoutines(any(DatasetId.class), any(BigQuery.RoutineListOption[].class)); assertNotNull(results); List resultList = new ArrayList<>(results); @@ -2931,11 +2931,14 @@ public void testGetSchemas_NoArgs_DelegatesCorrectly() throws SQLException { ResultSet mockResultSet = mock(ResultSet.class); doReturn(mockResultSet).when(spiedDbMetadata).getSchemas(null, null); - ResultSet rs = spiedDbMetadata.getSchemas(); + try (ResultSet rs = spiedDbMetadata.getSchemas()) { - assertSame( - mockResultSet, rs, "The returned ResultSet should be the one from the two-argument method"); - verify(spiedDbMetadata, times(1)).getSchemas(null, null); + assertSame( + mockResultSet, + rs, + "The returned ResultSet should be the one from the two-argument method"); + verify(spiedDbMetadata, times(1)).getSchemas(null, null); + } } // Non-Resultset DatabaseMetadata tests @@ -3262,18 +3265,19 @@ public void testGetCatalogs_WithProjectDiscovery() throws SQLException { .thenReturn(Arrays.asList("discovered-1", "discovered-2")); when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1,additional-2"); - ResultSet rs = dbMetadata.getCatalogs(); - assertNotNull(rs); + try (ResultSet rs = dbMetadata.getCatalogs()) { + assertNotNull(rs); - List catalogs = new ArrayList<>(); - while (rs.next()) { - catalogs.add(rs.getString("TABLE_CAT")); - } + List catalogs = new ArrayList<>(); + while (rs.next()) { + catalogs.add(rs.getString("TABLE_CAT")); + } - assertThat(catalogs) - .containsExactly( - "additional-1", "additional-2", "discovered-1", "discovered-2", "primary-project") - .inOrder(); + assertThat(catalogs) + .containsExactly( + "additional-1", "additional-2", "discovered-1", "discovered-2", "primary-project") + .inOrder(); + } } @Test @@ -3284,17 +3288,18 @@ public void testGetCatalogs_WithoutProjectDiscovery() throws SQLException { .thenReturn(Arrays.asList("discovered-1", "discovered-2")); when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1,additional-2"); - ResultSet rs = dbMetadata.getCatalogs(); - assertNotNull(rs); + try (ResultSet rs = dbMetadata.getCatalogs()) { + assertNotNull(rs); - List catalogs = new ArrayList<>(); - while (rs.next()) { - catalogs.add(rs.getString("TABLE_CAT")); - } + List catalogs = new ArrayList<>(); + while (rs.next()) { + catalogs.add(rs.getString("TABLE_CAT")); + } - assertThat(catalogs) - .containsExactly("additional-1", "additional-2", "primary-project") - .inOrder(); + assertThat(catalogs) + .containsExactly("additional-1", "additional-2", "primary-project") + .inOrder(); + } } @Test @@ -3307,37 +3312,39 @@ public void testGetSchemas_WithProjectDiscovery() throws SQLException { Page pagePrimary = mock(Page.class); Dataset dsPrimary = mockBigQueryDataset("primary-project", "dataset_p"); when(pagePrimary.iterateAll()).thenReturn(Collections.singletonList(dsPrimary)); - when(bigqueryClient.listDatasets(eq("primary-project"), any(BigQuery.DatasetListOption.class))) + when(bigqueryClient.listDatasets( + eq("primary-project"), any(BigQuery.DatasetListOption[].class))) .thenReturn(pagePrimary); Page pageAdditional = mock(Page.class); Dataset dsAdditional = mockBigQueryDataset("additional-1", "dataset_a"); when(pageAdditional.iterateAll()).thenReturn(Collections.singletonList(dsAdditional)); - when(bigqueryClient.listDatasets(eq("additional-1"), any(BigQuery.DatasetListOption.class))) + when(bigqueryClient.listDatasets(eq("additional-1"), any(BigQuery.DatasetListOption[].class))) .thenReturn(pageAdditional); Page pageDiscovered = mock(Page.class); Dataset dsDiscovered = mockBigQueryDataset("discovered-1", "dataset_d"); when(pageDiscovered.iterateAll()).thenReturn(Collections.singletonList(dsDiscovered)); - when(bigqueryClient.listDatasets(eq("discovered-1"), any(BigQuery.DatasetListOption.class))) + when(bigqueryClient.listDatasets(eq("discovered-1"), any(BigQuery.DatasetListOption[].class))) .thenReturn(pageDiscovered); - ResultSet rs = dbMetadata.getSchemas(null, null); - assertNotNull(rs); + try (ResultSet rs = dbMetadata.getSchemas(null, null)) { + assertNotNull(rs); - List schemas = new ArrayList<>(); - List catalogs = new ArrayList<>(); - while (rs.next()) { - schemas.add(rs.getString("TABLE_SCHEM")); - catalogs.add(rs.getString("TABLE_CATALOG")); - } + List schemas = new ArrayList<>(); + List catalogs = new ArrayList<>(); + while (rs.next()) { + schemas.add(rs.getString("TABLE_SCHEM")); + catalogs.add(rs.getString("TABLE_CATALOG")); + } - // Results are sorted by catalog (TABLE_CATALOG) then schema (TABLE_SCHEM) - // alphabetical catalog: "additional-1", "discovered-1", "primary-project" - assertThat(catalogs) - .containsExactly("additional-1", "discovered-1", "primary-project") - .inOrder(); - assertThat(schemas).containsExactly("dataset_a", "dataset_d", "dataset_p").inOrder(); + // Results are sorted by catalog (TABLE_CATALOG) then schema (TABLE_SCHEM) + // alphabetical catalog: "additional-1", "discovered-1", "primary-project" + assertThat(catalogs) + .containsExactly("additional-1", "discovered-1", "primary-project") + .inOrder(); + assertThat(schemas).containsExactly("dataset_a", "dataset_d", "dataset_p").inOrder(); + } } @Test @@ -3350,31 +3357,249 @@ public void testGetSchemas_WithoutProjectDiscovery() throws SQLException { Page pagePrimary = mock(Page.class); Dataset dsPrimary = mockBigQueryDataset("primary-project", "dataset_p"); when(pagePrimary.iterateAll()).thenReturn(Collections.singletonList(dsPrimary)); - when(bigqueryClient.listDatasets(eq("primary-project"), any(BigQuery.DatasetListOption.class))) + when(bigqueryClient.listDatasets( + eq("primary-project"), any(BigQuery.DatasetListOption[].class))) .thenReturn(pagePrimary); Page pageAdditional = mock(Page.class); Dataset dsAdditional = mockBigQueryDataset("additional-1", "dataset_a"); when(pageAdditional.iterateAll()).thenReturn(Collections.singletonList(dsAdditional)); - when(bigqueryClient.listDatasets(eq("additional-1"), any(BigQuery.DatasetListOption.class))) + when(bigqueryClient.listDatasets(eq("additional-1"), any(BigQuery.DatasetListOption[].class))) .thenReturn(pageAdditional); - ResultSet rs = dbMetadata.getSchemas(null, null); - assertNotNull(rs); + try (ResultSet rs = dbMetadata.getSchemas(null, null)) { + assertNotNull(rs); + + List schemas = new ArrayList<>(); + List catalogs = new ArrayList<>(); + while (rs.next()) { + schemas.add(rs.getString("TABLE_SCHEM")); + catalogs.add(rs.getString("TABLE_CATALOG")); + } + + // Results are sorted by catalog (TABLE_CATALOG) then schema (TABLE_SCHEM) + // alphabetical catalog: "additional-1", "primary-project" (discovered-1 is ignored) + assertThat(catalogs).containsExactly("additional-1", "primary-project").inOrder(); + assertThat(schemas).containsExactly("dataset_a", "dataset_p").inOrder(); - List schemas = new ArrayList<>(); - List catalogs = new ArrayList<>(); - while (rs.next()) { - schemas.add(rs.getString("TABLE_SCHEM")); - catalogs.add(rs.getString("TABLE_CATALOG")); + verify(bigqueryClient, never()) + .listDatasets(eq("discovered-1"), any(BigQuery.DatasetListOption[].class)); } + } - // Results are sorted by catalog (TABLE_CATALOG) then schema (TABLE_SCHEM) - // alphabetical catalog: "additional-1", "primary-project" (discovered-1 is ignored) - assertThat(catalogs).containsExactly("additional-1", "primary-project").inOrder(); - assertThat(schemas).containsExactly("dataset_a", "dataset_p").inOrder(); + private void mockDatasetIteration(DatasetId datasetId) { + Page pagePrimary = mock(Page.class); + Dataset dsPrimary = mock(Dataset.class); + when(dsPrimary.getDatasetId()).thenReturn(datasetId); + when(pagePrimary.iterateAll()).thenReturn(Collections.singletonList(dsPrimary)); + when(bigqueryClient.listDatasets( + eq(datasetId.getProject()), any(BigQuery.DatasetListOption[].class))) + .thenReturn(pagePrimary); + } - verify(bigqueryClient, never()) - .listDatasets(eq("discovered-1"), any(BigQuery.DatasetListOption.class)); + private Table mockTableWithConstraints(TableId tableId, TableConstraints constraints) { + Table mockTable = mock(Table.class); + when(mockTable.getTableId()).thenReturn(tableId); + TableDefinition mockDef = mock(TableDefinition.class); + when(mockDef.getType()).thenReturn(TableDefinition.Type.TABLE); + when(mockTable.getDefinition()).thenReturn(mockDef); + if (constraints != null) { + when(mockTable.getTableConstraints()).thenReturn(constraints); + } + when(bigqueryClient.getTable(eq(tableId))).thenReturn(mockTable); + return mockTable; + } + + private void mockTableIteration(DatasetId datasetId, Table... tables) { + Page
pageTables = mock(Page.class); + when(pageTables.iterateAll()).thenReturn(Arrays.asList(tables)); + when(bigqueryClient.listTables(eq(datasetId), any(BigQuery.TableListOption[].class))) + .thenReturn(pageTables); + } + + @Test + public void testGetPrimaryKeys_hasKey() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + + TableConstraints mockConstraints = mock(TableConstraints.class); + PrimaryKey mockPk = mock(PrimaryKey.class); + when(mockPk.getColumns()).thenReturn(Arrays.asList("id_col")); + when(mockConstraints.getPrimaryKey()).thenReturn(mockPk); + + mockTableWithConstraints(tableId, mockConstraints); + mockDatasetIteration(datasetId); + + try (ResultSet rs = dbMetadata.getPrimaryKeys("test-project", "dataset_p", "table_p")) { + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("TABLE_CAT")); + assertEquals("dataset_p", rs.getString("TABLE_SCHEM")); + assertEquals("table_p", rs.getString("TABLE_NAME")); + assertEquals("id_col", rs.getString("COLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertNull(rs.getString("PK_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetPrimaryKeys_noKeys() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + + mockTableWithConstraints(tableId, null); + mockDatasetIteration(datasetId); + + try (ResultSet rs = dbMetadata.getPrimaryKeys("test-project", "dataset_p", "table_p")) { + assertFalse(rs.next()); + } + } + + @Test + public void testGetImportedKeys_hasKeys() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + + TableConstraints mockConstraints = mock(TableConstraints.class); + ForeignKey mockFk = mock(ForeignKey.class); + when(mockFk.getName()).thenReturn("fk_name"); + when(mockFk.getReferencedTable()) + .thenReturn(TableId.of("test-project", "dataset_p", "ref_table")); + + ColumnReference mockRef = mock(ColumnReference.class); + when(mockRef.getReferencingColumn()).thenReturn("fk_col"); + when(mockRef.getReferencedColumn()).thenReturn("pk_col"); + when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + + when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); + mockTableWithConstraints(tableId, mockConstraints); + mockDatasetIteration(datasetId); + + try (ResultSet rs = dbMetadata.getImportedKeys("test-project", "dataset_p", "table_p")) { + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("ref_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("table_p", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetImportedKeys_noKeys() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + + mockTableWithConstraints(tableId, null); + mockDatasetIteration(datasetId); + + try (ResultSet rs = dbMetadata.getImportedKeys("test-project", "dataset_p", "table_p")) { + assertFalse(rs.next()); + } + } + + @Test + public void testGetExportedKeys_hasKeys() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + TableId refTableId = TableId.of("test-project", "dataset_p", "ref_table"); + + TableConstraints mockConstraints = mock(TableConstraints.class); + ForeignKey mockFk = mock(ForeignKey.class); + when(mockFk.getName()).thenReturn("fk_name"); + when(mockFk.getReferencedTable()).thenReturn(refTableId); + ColumnReference mockRef = mock(ColumnReference.class); + when(mockRef.getReferencingColumn()).thenReturn("fk_col"); + when(mockRef.getReferencedColumn()).thenReturn("pk_col"); + when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); + + Table mockTableP = mockTableWithConstraints(tableId, mockConstraints); + mockDatasetIteration(datasetId); + mockTableIteration(datasetId, mockTableP); + + try (ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset_p", "ref_table")) { + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("ref_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("table_p", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetExportedKeys_noKeys() throws SQLException { + DatasetId datasetId = DatasetId.of("test-project", "dataset_p"); + TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); + + Table mockTableP = mockTableWithConstraints(tableId, null); + mockDatasetIteration(datasetId); + mockTableIteration(datasetId, mockTableP); + + try (ResultSet rs = dbMetadata.getExportedKeys("test-project", "dataset_p", "ref_table")) { + assertFalse(rs.next()); + } + } + + @Test + public void testGetCrossReference_hasKeys() throws SQLException { + TableId fkTableId = TableId.of("test-project", "dataset_p", "fk_table"); + TableId pkTableId = TableId.of("test-project", "dataset_p", "pk_table"); + + TableConstraints mockConstraints = mock(TableConstraints.class); + ForeignKey mockFk = mock(ForeignKey.class); + when(mockFk.getName()).thenReturn("fk_name"); + when(mockFk.getReferencedTable()).thenReturn(pkTableId); + + ColumnReference mockRef = mock(ColumnReference.class); + when(mockRef.getReferencingColumn()).thenReturn("fk_col"); + when(mockRef.getReferencedColumn()).thenReturn("pk_col"); + when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); + + mockTableWithConstraints(fkTableId, mockConstraints); + + try (ResultSet rs = + dbMetadata.getCrossReference( + "test-project", "dataset_p", "pk_table", "test-project", "dataset_p", "fk_table")) { + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("pk_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("fk_table", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetCrossReference_noKeys() throws SQLException { + TableId fkTableId = TableId.of("test-project", "dataset_p", "fk_table"); + + mockTableWithConstraints(fkTableId, null); + + try (ResultSet rs = + dbMetadata.getCrossReference( + "test-project", "dataset_p", "pk_table", "test-project", "dataset_p", "fk_table")) { + assertFalse(rs.next()); + } } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java index e40ba8bccd8c..046f49991dbe 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java @@ -329,6 +329,64 @@ public void testTableConstraints() throws SQLException { connection.close(); } + @Test + public void testGetExportedKeys_multipleKeys() throws SQLException { + try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl)) { + DatabaseMetaData metaData = connection.getMetaData(); + + // Table 2 exports keys to Table + ResultSet exportedKeys2 = + metaData.getExportedKeys(PROJECT_ID, CONSTRAINTS_DATASET, CONSTRAINTS_TABLE_NAME2); + Assertions.assertTrue(exportedKeys2.next()); + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME2, exportedKeys2.getString(3)); // PKTABLE_NAME + Assertions.assertEquals("first_name", exportedKeys2.getString(4)); // PKCOLUMN_NAME + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME, exportedKeys2.getString(7)); // FKTABLE_NAME + Assertions.assertEquals("name", exportedKeys2.getString(8)); // FKCOLUMN_NAME + Assertions.assertEquals(1, exportedKeys2.getInt(9)); // KEY_SEQ + Assertions.assertEquals("my_fk", exportedKeys2.getString(12)); // FK_NAME + + Assertions.assertTrue(exportedKeys2.next()); + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME2, exportedKeys2.getString(3)); + Assertions.assertEquals("last_name", exportedKeys2.getString(4)); + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME, exportedKeys2.getString(7)); + Assertions.assertEquals("second_name", exportedKeys2.getString(8)); + Assertions.assertEquals(2, exportedKeys2.getInt(9)); + Assertions.assertEquals("my_fk", exportedKeys2.getString(12)); + Assertions.assertFalse(exportedKeys2.next()); + } + } + + @Test + public void testGetExportedKeys_singleKey() throws SQLException { + try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl)) { + DatabaseMetaData metaData = connection.getMetaData(); + + // Table 3 exports keys to Table + ResultSet exportedKeys3 = + metaData.getExportedKeys(PROJECT_ID, CONSTRAINTS_DATASET, CONSTRAINTS_TABLE_NAME3); + Assertions.assertTrue(exportedKeys3.next()); + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME3, exportedKeys3.getString(3)); + Assertions.assertEquals("address", exportedKeys3.getString(4)); + Assertions.assertEquals(CONSTRAINTS_TABLE_NAME, exportedKeys3.getString(7)); + Assertions.assertEquals("address", exportedKeys3.getString(8)); + Assertions.assertEquals(1, exportedKeys3.getInt(9)); + Assertions.assertEquals("my_fk2", exportedKeys3.getString(12)); + Assertions.assertFalse(exportedKeys3.next()); + } + } + + @Test + public void testGetExportedKeys_noKeys() throws SQLException { + try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl)) { + DatabaseMetaData metaData = connection.getMetaData(); + + // Table does not export keys to anything (it only imports them) + ResultSet exportedKeys1 = + metaData.getExportedKeys(PROJECT_ID, CONSTRAINTS_DATASET, CONSTRAINTS_TABLE_NAME); + Assertions.assertFalse(exportedKeys1.next()); + } + } + @Test public void testMetadataResultSetsDoNotInterfere() throws SQLException { try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl)) { From a095432e8090e77fc6ad8ff0e658ab842d3df747 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 9 Jul 2026 14:21:16 +0000 Subject: [PATCH 2/2] add multiple columns for test assertions --- .../jdbc/BigQueryDatabaseMetaDataTest.java | 93 ++++++++++++++----- 1 file changed, 69 insertions(+), 24 deletions(-) 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 e0904eb7fdde..7264b3349772 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 @@ -3424,20 +3424,26 @@ public void testGetPrimaryKeys_hasKey() throws SQLException { TableConstraints mockConstraints = mock(TableConstraints.class); PrimaryKey mockPk = mock(PrimaryKey.class); - when(mockPk.getColumns()).thenReturn(Arrays.asList("id_col")); + when(mockPk.getColumns()).thenReturn(Arrays.asList("id_col1", "id_col2")); when(mockConstraints.getPrimaryKey()).thenReturn(mockPk); mockTableWithConstraints(tableId, mockConstraints); - mockDatasetIteration(datasetId); try (ResultSet rs = dbMetadata.getPrimaryKeys("test-project", "dataset_p", "table_p")) { assertTrue(rs.next()); assertEquals("test-project", rs.getString("TABLE_CAT")); assertEquals("dataset_p", rs.getString("TABLE_SCHEM")); assertEquals("table_p", rs.getString("TABLE_NAME")); - assertEquals("id_col", rs.getString("COLUMN_NAME")); + assertEquals("id_col1", rs.getString("COLUMN_NAME")); assertEquals(1, rs.getInt("KEY_SEQ")); assertNull(rs.getString("PK_NAME")); + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("TABLE_CAT")); + assertEquals("dataset_p", rs.getString("TABLE_SCHEM")); + assertEquals("table_p", rs.getString("TABLE_NAME")); + assertEquals("id_col2", rs.getString("COLUMN_NAME")); + assertEquals(2, rs.getInt("KEY_SEQ")); + assertNull(rs.getString("PK_NAME")); assertFalse(rs.next()); } } @@ -3448,7 +3454,6 @@ public void testGetPrimaryKeys_noKeys() throws SQLException { TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); mockTableWithConstraints(tableId, null); - mockDatasetIteration(datasetId); try (ResultSet rs = dbMetadata.getPrimaryKeys("test-project", "dataset_p", "table_p")) { assertFalse(rs.next()); @@ -3466,27 +3471,40 @@ public void testGetImportedKeys_hasKeys() throws SQLException { when(mockFk.getReferencedTable()) .thenReturn(TableId.of("test-project", "dataset_p", "ref_table")); - ColumnReference mockRef = mock(ColumnReference.class); - when(mockRef.getReferencingColumn()).thenReturn("fk_col"); - when(mockRef.getReferencedColumn()).thenReturn("pk_col"); - when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + ColumnReference mockRef1 = mock(ColumnReference.class); + when(mockRef1.getReferencingColumn()).thenReturn("fk_col1"); + when(mockRef1.getReferencedColumn()).thenReturn("pk_col1"); + ColumnReference mockRef2 = mock(ColumnReference.class); + when(mockRef2.getReferencingColumn()).thenReturn("fk_col2"); + when(mockRef2.getReferencedColumn()).thenReturn("pk_col2"); + when(mockFk.getColumnReferences()).thenReturn(Arrays.asList(mockRef1, mockRef2)); when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); mockTableWithConstraints(tableId, mockConstraints); - mockDatasetIteration(datasetId); try (ResultSet rs = dbMetadata.getImportedKeys("test-project", "dataset_p", "table_p")) { assertTrue(rs.next()); assertEquals("test-project", rs.getString("PKTABLE_CAT")); assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); assertEquals("ref_table", rs.getString("PKTABLE_NAME")); - assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("pk_col1", rs.getString("PKCOLUMN_NAME")); assertEquals("test-project", rs.getString("FKTABLE_CAT")); assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); assertEquals("table_p", rs.getString("FKTABLE_NAME")); - assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals("fk_col1", rs.getString("FKCOLUMN_NAME")); assertEquals(1, rs.getInt("KEY_SEQ")); assertEquals("fk_name", rs.getString("FK_NAME")); + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("ref_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col2", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("table_p", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col2", rs.getString("FKCOLUMN_NAME")); + assertEquals(2, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); assertFalse(rs.next()); } } @@ -3497,7 +3515,6 @@ public void testGetImportedKeys_noKeys() throws SQLException { TableId tableId = TableId.of("test-project", "dataset_p", "table_p"); mockTableWithConstraints(tableId, null); - mockDatasetIteration(datasetId); try (ResultSet rs = dbMetadata.getImportedKeys("test-project", "dataset_p", "table_p")) { assertFalse(rs.next()); @@ -3514,10 +3531,13 @@ public void testGetExportedKeys_hasKeys() throws SQLException { ForeignKey mockFk = mock(ForeignKey.class); when(mockFk.getName()).thenReturn("fk_name"); when(mockFk.getReferencedTable()).thenReturn(refTableId); - ColumnReference mockRef = mock(ColumnReference.class); - when(mockRef.getReferencingColumn()).thenReturn("fk_col"); - when(mockRef.getReferencedColumn()).thenReturn("pk_col"); - when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + ColumnReference mockRef1 = mock(ColumnReference.class); + when(mockRef1.getReferencingColumn()).thenReturn("fk_col1"); + when(mockRef1.getReferencedColumn()).thenReturn("pk_col1"); + ColumnReference mockRef2 = mock(ColumnReference.class); + when(mockRef2.getReferencingColumn()).thenReturn("fk_col2"); + when(mockRef2.getReferencedColumn()).thenReturn("pk_col2"); + when(mockFk.getColumnReferences()).thenReturn(Arrays.asList(mockRef1, mockRef2)); when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); Table mockTableP = mockTableWithConstraints(tableId, mockConstraints); @@ -3529,13 +3549,24 @@ public void testGetExportedKeys_hasKeys() throws SQLException { assertEquals("test-project", rs.getString("PKTABLE_CAT")); assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); assertEquals("ref_table", rs.getString("PKTABLE_NAME")); - assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("pk_col1", rs.getString("PKCOLUMN_NAME")); assertEquals("test-project", rs.getString("FKTABLE_CAT")); assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); assertEquals("table_p", rs.getString("FKTABLE_NAME")); - assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals("fk_col1", rs.getString("FKCOLUMN_NAME")); assertEquals(1, rs.getInt("KEY_SEQ")); assertEquals("fk_name", rs.getString("FK_NAME")); + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("ref_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col2", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("table_p", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col2", rs.getString("FKCOLUMN_NAME")); + assertEquals(2, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); assertFalse(rs.next()); } } @@ -3564,10 +3595,13 @@ public void testGetCrossReference_hasKeys() throws SQLException { when(mockFk.getName()).thenReturn("fk_name"); when(mockFk.getReferencedTable()).thenReturn(pkTableId); - ColumnReference mockRef = mock(ColumnReference.class); - when(mockRef.getReferencingColumn()).thenReturn("fk_col"); - when(mockRef.getReferencedColumn()).thenReturn("pk_col"); - when(mockFk.getColumnReferences()).thenReturn(Collections.singletonList(mockRef)); + ColumnReference mockRef1 = mock(ColumnReference.class); + when(mockRef1.getReferencingColumn()).thenReturn("fk_col1"); + when(mockRef1.getReferencedColumn()).thenReturn("pk_col1"); + ColumnReference mockRef2 = mock(ColumnReference.class); + when(mockRef2.getReferencingColumn()).thenReturn("fk_col2"); + when(mockRef2.getReferencedColumn()).thenReturn("pk_col2"); + when(mockFk.getColumnReferences()).thenReturn(Arrays.asList(mockRef1, mockRef2)); when(mockConstraints.getForeignKeys()).thenReturn(Collections.singletonList(mockFk)); mockTableWithConstraints(fkTableId, mockConstraints); @@ -3579,13 +3613,24 @@ public void testGetCrossReference_hasKeys() throws SQLException { assertEquals("test-project", rs.getString("PKTABLE_CAT")); assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); assertEquals("pk_table", rs.getString("PKTABLE_NAME")); - assertEquals("pk_col", rs.getString("PKCOLUMN_NAME")); + assertEquals("pk_col1", rs.getString("PKCOLUMN_NAME")); assertEquals("test-project", rs.getString("FKTABLE_CAT")); assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); assertEquals("fk_table", rs.getString("FKTABLE_NAME")); - assertEquals("fk_col", rs.getString("FKCOLUMN_NAME")); + assertEquals("fk_col1", rs.getString("FKCOLUMN_NAME")); assertEquals(1, rs.getInt("KEY_SEQ")); assertEquals("fk_name", rs.getString("FK_NAME")); + assertTrue(rs.next()); + assertEquals("test-project", rs.getString("PKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("PKTABLE_SCHEM")); + assertEquals("pk_table", rs.getString("PKTABLE_NAME")); + assertEquals("pk_col2", rs.getString("PKCOLUMN_NAME")); + assertEquals("test-project", rs.getString("FKTABLE_CAT")); + assertEquals("dataset_p", rs.getString("FKTABLE_SCHEM")); + assertEquals("fk_table", rs.getString("FKTABLE_NAME")); + assertEquals("fk_col2", rs.getString("FKCOLUMN_NAME")); + assertEquals(2, rs.getInt("KEY_SEQ")); + assertEquals("fk_name", rs.getString("FK_NAME")); assertFalse(rs.next()); } }