diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java index 1146f4d46d95..ce1e3abf1723 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java @@ -19,18 +19,21 @@ import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.api.gax.paging.Page; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.FixedHeaderProvider; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.auth.Credentials; import com.google.cloud.bigquery.BigQuery; +import com.google.cloud.bigquery.BigQuery.ProjectListOption; import com.google.cloud.bigquery.BigQueryException; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.ConnectionProperty; import com.google.cloud.bigquery.DatasetId; import com.google.cloud.bigquery.Job; import com.google.cloud.bigquery.JobInfo; +import com.google.cloud.bigquery.Project; import com.google.cloud.bigquery.QueryJobConfiguration; import com.google.cloud.bigquery.QueryJobConfiguration.JobCreationMode; import com.google.cloud.bigquery.exception.BigQueryJdbcException; @@ -41,6 +44,7 @@ import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient; import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings; import com.google.cloud.http.HttpTransportOptions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedSet; import java.io.IOException; import java.io.InputStream; @@ -121,6 +125,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { BigQueryJdbcUrlUtility.SWA_APPEND_ROW_COUNT_PROPERTY_NAME, BigQueryJdbcUrlUtility.SWA_ACTIVATION_ROW_COUNT_PROPERTY_NAME, BigQueryJdbcUrlUtility.FILTER_TABLES_ON_DEFAULT_DATASET_PROPERTY_NAME, + BigQueryJdbcUrlUtility.ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME, BigQueryJdbcUrlUtility.REQUEST_GOOGLE_DRIVE_SCOPE_PROPERTY_NAME, BigQueryJdbcUrlUtility.SSL_TRUST_STORE_PROPERTY_NAME, BigQueryJdbcUrlUtility.MAX_BYTES_BILLED_PROPERTY_NAME, @@ -170,6 +175,8 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { int highThroughputMinTableSize; int highThroughputActivationRatio; boolean enableSession; + boolean enableProjectDiscovery; + private List discoveredProjectsCache; boolean unsupportedHTAPIFallback; boolean useQueryCache; String queryDialect; @@ -338,6 +345,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { this.additionalProjects = ds.getAdditionalProjects(); this.filterTablesOnDefaultDataset = ds.getFilterTablesOnDefaultDataset(); + this.enableProjectDiscovery = ds.getEnableProjectDiscovery(); this.requestGoogleDriveScope = ds.getRequestGoogleDriveScope(); this.metadataFetchThreadCount = ds.getMetadataFetchThreadCount(); this.requestReason = ds.getRequestReason(); @@ -1312,6 +1320,39 @@ private boolean checkIsReadOnlyTokenUsed(Map authProps) { return false; } + public boolean isEnableProjectDiscovery() { + return this.enableProjectDiscovery; + } + + public synchronized List getDiscoveredProjects() { + if (this.discoveredProjectsCache != null) { + return this.discoveredProjectsCache; + } + + try { + BigQuery bigQuery = getBigQuery(); + List projects = new ArrayList<>(); + Page projectPage = + bigQuery.listProjects(ProjectListOption.pageSize(getMaxResults())); + for (Project project : projectPage.iterateAll()) { + projects.add(project.getProjectId()); + } + this.discoveredProjectsCache = ImmutableList.copyOf(projects); + } catch (BigQueryException e) { + LOG.warning(e, "Failed to list all accessible projects due to BigQuery error."); + int statusCode = e.getCode(); + // Only cache empty list for non-transient auth/permission errors (400, 401, 403) + if (statusCode == 400 || statusCode == 401 || statusCode == 403) { + this.discoveredProjectsCache = ImmutableList.of(); + } + return ImmutableList.of(); + } catch (Exception e) { + LOG.warning(e, "Failed to list all accessible projects, falling back to connection default."); + return ImmutableList.of(); + } + return this.discoveredProjectsCache; + } + @Override public T unwrap(Class iface) throws SQLException { if (iface.isInstance(this)) { 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 73da585db087..19e9668b85c7 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 @@ -5199,6 +5199,10 @@ private List getAccessibleCatalogNames() { } } + if (this.connection.isEnableProjectDiscovery()) { + accessibleCatalogs.addAll(this.connection.getDiscoveredProjects()); + } + List sortedCatalogs = new ArrayList<>(accessibleCatalogs); Collections.sort(sortedCatalogs); return sortedCatalogs; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index dd46dae44188..a46a4eecd4f7 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -168,6 +168,8 @@ protected boolean removeEldestEntry(Map.Entry> eldes static final String FILTER_TABLES_ON_DEFAULT_DATASET_PROPERTY_NAME = "FilterTablesOnDefaultDataset"; static final boolean DEFAULT_FILTER_TABLES_ON_DEFAULT_DATASET_VALUE = false; + static final String ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME = "EnableProjectDiscovery"; + static final boolean DEFAULT_ENABLE_PROJECT_DISCOVERY_VALUE = false; static final String REQUEST_GOOGLE_DRIVE_SCOPE_PROPERTY_NAME = "RequestGoogleDriveScope"; static final String SSL_TRUST_STORE_PROPERTY_NAME = "SSLTrustStore"; static final String SSL_TRUST_STORE_PWD_PROPERTY_NAME = "SSLTrustStorePwd"; @@ -577,6 +579,13 @@ protected boolean removeEldestEntry(Map.Entry> eldes .setDefaultValue( String.valueOf(DEFAULT_FILTER_TABLES_ON_DEFAULT_DATASET_VALUE)) .build(), + BigQueryConnectionProperty.newBuilder() + .setName(ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME) + .setDescription( + "Enables or disables automatic discovery of all accessible Google Cloud projects. " + + "When disabled, only the default ProjectId and AdditionalProjects are listed as catalogs.") + .setDefaultValue(String.valueOf(DEFAULT_ENABLE_PROJECT_DISCOVERY_VALUE)) + .build(), BigQueryConnectionProperty.newBuilder() .setName(REQUEST_GOOGLE_DRIVE_SCOPE_PROPERTY_NAME) .setDescription( diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index 9e82fee605b5..e51ebeb00e90 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -84,6 +84,7 @@ public class DataSource implements javax.sql.DataSource { private Boolean enableWriteAPI; private String additionalProjects; private Boolean filterTablesOnDefaultDataset; + private Boolean enableProjectDiscovery; private Integer requestGoogleDriveScope; private Integer metadataFetchThreadCount; private String sslTrustStorePath; @@ -242,6 +243,12 @@ public class DataSource implements javax.sql.DataSource { BigQueryJdbcUrlUtility.convertIntToBoolean( val, BigQueryJdbcUrlUtility.FILTER_TABLES_ON_DEFAULT_DATASET_PROPERTY_NAME))) + .put( + BigQueryJdbcUrlUtility.ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME, + (ds, val) -> + ds.setEnableProjectDiscovery( + BigQueryJdbcUrlUtility.convertIntToBoolean( + val, BigQueryJdbcUrlUtility.ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME))) .put( BigQueryJdbcUrlUtility.REQUEST_GOOGLE_DRIVE_SCOPE_PROPERTY_NAME, (ds, val) -> ds.setRequestGoogleDriveScope(Integer.parseInt(val))) @@ -555,6 +562,11 @@ Properties createProperties() { BigQueryJdbcUrlUtility.FILTER_TABLES_ON_DEFAULT_DATASET_PROPERTY_NAME, String.valueOf(this.filterTablesOnDefaultDataset)); } + if (this.enableProjectDiscovery != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.ENABLE_PROJECT_DISCOVERY_PROPERTY_NAME, + String.valueOf(this.enableProjectDiscovery)); + } if (this.requestGoogleDriveScope != null) { connectionProperties.setProperty( BigQueryJdbcUrlUtility.REQUEST_GOOGLE_DRIVE_SCOPE_PROPERTY_NAME, @@ -1060,6 +1072,16 @@ public void setFilterTablesOnDefaultDataset(Boolean filterTablesOnDefaultDataset this.filterTablesOnDefaultDataset = filterTablesOnDefaultDataset; } + public Boolean getEnableProjectDiscovery() { + return enableProjectDiscovery != null + ? enableProjectDiscovery + : BigQueryJdbcUrlUtility.DEFAULT_ENABLE_PROJECT_DISCOVERY_VALUE; + } + + public void setEnableProjectDiscovery(Boolean enableProjectDiscovery) { + this.enableProjectDiscovery = enableProjectDiscovery; + } + public Integer getRequestGoogleDriveScope() { return requestGoogleDriveScope != null ? requestGoogleDriveScope diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index 94cde20fa400..bcc2b64df623 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -23,11 +23,19 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.api.gax.paging.Page; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.cloud.bigquery.BigQuery; +import com.google.cloud.bigquery.BigQueryException; +import com.google.cloud.bigquery.Project; import com.google.cloud.bigquery.QueryJobConfiguration.JobCreationMode; import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.storage.v1.BigQueryReadClient; @@ -35,6 +43,8 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.Properties; import java.util.logging.Level; @@ -519,4 +529,69 @@ public void testWrapperMethods() throws Exception { assertTrue(e.getMessage().contains("Cannot unwrap to java.sql.Statement")); } } + + @Test + public void testGetDiscoveredProjects_Success() throws Exception { + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL)) { + BigQuery mockBigQuery = mock(BigQuery.class); + connection.bigQuery = mockBigQuery; + + Page mockPage = mock(Page.class); + Project project1 = mock(Project.class); + when(project1.getProjectId()).thenReturn("discovered-p1"); + Project project2 = mock(Project.class); + when(project2.getProjectId()).thenReturn("discovered-p2"); + + when(mockPage.iterateAll()).thenReturn(Arrays.asList(project1, project2)); + when(mockBigQuery.listProjects(any(BigQuery.ProjectListOption.class))).thenReturn(mockPage); + + List discovered = connection.getDiscoveredProjects(); + assertEquals(Arrays.asList("discovered-p1", "discovered-p2"), discovered); + + // Verify caching: second call should not invoke listProjects again + List discoveredCached = connection.getDiscoveredProjects(); + assertSame(discovered, discoveredCached); + verify(mockBigQuery, times(1)).listProjects(any(BigQuery.ProjectListOption.class)); + } + } + + @Test + public void testGetDiscoveredProjects_NonTransientError() throws Exception { + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL)) { + BigQuery mockBigQuery = mock(BigQuery.class); + connection.bigQuery = mockBigQuery; + + // 403 Forbidden (Non-transient error) + BigQueryException exception = new BigQueryException(403, "Access Denied"); + when(mockBigQuery.listProjects(any(BigQuery.ProjectListOption.class))).thenThrow(exception); + + List discovered = connection.getDiscoveredProjects(); + assertTrue(discovered.isEmpty()); + + // Verify that it caches the empty list for 403, so it does not retry. + List discoveredCached = connection.getDiscoveredProjects(); + assertTrue(discoveredCached.isEmpty()); + verify(mockBigQuery, times(1)).listProjects(any(BigQuery.ProjectListOption.class)); + } + } + + @Test + public void testGetDiscoveredProjects_TransientError() throws Exception { + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL)) { + BigQuery mockBigQuery = mock(BigQuery.class); + connection.bigQuery = mockBigQuery; + + // 500 Internal Error (Transient error, should not cache empty list) + BigQueryException exception = new BigQueryException(500, "Internal Server Error"); + when(mockBigQuery.listProjects(any(BigQuery.ProjectListOption.class))).thenThrow(exception); + + List discovered = connection.getDiscoveredProjects(); + assertTrue(discovered.isEmpty()); + + // Since it's a transient error, it should NOT cache the empty list and should try again. + List discoveredCached = connection.getDiscoveredProjects(); + assertTrue(discoveredCached.isEmpty()); + verify(mockBigQuery, times(2)).listProjects(any(BigQuery.ProjectListOption.class)); + } + } } 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 9b2b82644c35..8b3fb66deedb 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 @@ -3313,6 +3313,130 @@ public void testMetadataAndResultSetMetadataTypeMappingConsistency(StandardSQLTy metadataTypeInfo.jdbcType, (int) resultSetType, "Type mapping mismatch for " + type); } + @Test + public void testGetCatalogs_WithProjectDiscovery() throws SQLException { + when(bigQueryConnection.getCatalog()).thenReturn("primary-project"); + when(bigQueryConnection.isEnableProjectDiscovery()).thenReturn(true); + when(bigQueryConnection.getDiscoveredProjects()) + .thenReturn(Arrays.asList("discovered-1", "discovered-2")); + when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1,additional-2"); + + ResultSet rs = dbMetadata.getCatalogs(); + assertNotNull(rs); + + 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(); + } + + @Test + public void testGetCatalogs_WithoutProjectDiscovery() throws SQLException { + when(bigQueryConnection.getCatalog()).thenReturn("primary-project"); + when(bigQueryConnection.isEnableProjectDiscovery()).thenReturn(false); + when(bigQueryConnection.getDiscoveredProjects()) + .thenReturn(Arrays.asList("discovered-1", "discovered-2")); + when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1,additional-2"); + + ResultSet rs = dbMetadata.getCatalogs(); + assertNotNull(rs); + + List catalogs = new ArrayList<>(); + while (rs.next()) { + catalogs.add(rs.getString("TABLE_CAT")); + } + + assertThat(catalogs) + .containsExactly("additional-1", "additional-2", "primary-project") + .inOrder(); + } + + @Test + public void testGetSchemas_WithProjectDiscovery() throws SQLException { + when(bigQueryConnection.getCatalog()).thenReturn("primary-project"); + when(bigQueryConnection.isEnableProjectDiscovery()).thenReturn(true); + when(bigQueryConnection.getDiscoveredProjects()).thenReturn(Arrays.asList("discovered-1")); + when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1"); + + 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))) + .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))) + .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))) + .thenReturn(pageDiscovered); + + 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", "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 + public void testGetSchemas_WithoutProjectDiscovery() throws SQLException { + when(bigQueryConnection.getCatalog()).thenReturn("primary-project"); + when(bigQueryConnection.isEnableProjectDiscovery()).thenReturn(false); + when(bigQueryConnection.getDiscoveredProjects()).thenReturn(Arrays.asList("discovered-1")); + when(bigQueryConnection.getAdditionalProjects()).thenReturn("additional-1"); + + 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))) + .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))) + .thenReturn(pageAdditional); + + 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(); + + verify(bigqueryClient, never()) + .listDatasets(eq("discovered-1"), any(BigQuery.DatasetListOption.class)); + } + @Test public void testWrapThread_NullThread() { assertNull(BigQueryDatabaseMetaData.wrapThread(null)); diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 3a09813a035e..0bc580391b12 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -260,4 +260,23 @@ public void testUnrecognizedConnectionProperties() { String url2 = "jdbc:bigquery://;MalformedProperty"; assertThrows(BigQueryJdbcRuntimeException.class, () -> DataSource.fromUrl(url2)); } + + @Test + public void testParseEnableProjectDiscovery() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "EnableProjectDiscovery=true"; + + String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "EnableProjectDiscovery"); + assertThat(result).isEqualTo("true"); + + String url2 = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "EnableProjectDiscovery=false"; + + String result2 = BigQueryJdbcUrlUtility.parseUriProperty(url2, "EnableProjectDiscovery"); + assertThat(result2).isEqualTo("false"); + } }