Skip to content

Commit be530bb

Browse files
[18713] Destination-bigquery: updated Check method to test for non-billable project (#19489)
* [18713] Destination-bigquery: updated Check method to test for non-billable project * fixed typo in comments * Bumped version * auto-bump connector version * auto-bump connector version Co-authored-by: Octavia Squidington III <[email protected]>
1 parent ccda38b commit be530bb

File tree

7 files changed

+113
-17
lines changed

7 files changed

+113
-17
lines changed

airbyte-config/init/src/main/resources/seed/destination_definitions.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
- name: BigQuery
4040
destinationDefinitionId: 22f6c74f-5699-40ff-833c-4a879ea40133
4141
dockerRepository: airbyte/destination-bigquery
42-
dockerImageTag: 1.2.7
42+
dockerImageTag: 1.2.8
4343
documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery
4444
icon: bigquery.svg
4545
normalizationRepository: airbyte/normalization
@@ -55,7 +55,7 @@
5555
- name: BigQuery (denormalized typed struct)
5656
destinationDefinitionId: 079d5540-f236-4294-ba7c-ade8fd918496
5757
dockerRepository: airbyte/destination-bigquery-denormalized
58-
dockerImageTag: 1.2.7
58+
dockerImageTag: 1.2.8
5959
documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery
6060
icon: bigquery.svg
6161
normalizationRepository: airbyte/normalization

airbyte-config/init/src/main/resources/seed/destination_specs.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@
621621
supported_destination_sync_modes:
622622
- "overwrite"
623623
- "append"
624-
- dockerImage: "airbyte/destination-bigquery:1.2.7"
624+
- dockerImage: "airbyte/destination-bigquery:1.2.8"
625625
spec:
626626
documentationUrl: "https://docs.airbyte.com/integrations/destinations/bigquery"
627627
connectionSpecification:
@@ -831,7 +831,7 @@
831831
- "overwrite"
832832
- "append"
833833
- "append_dedup"
834-
- dockerImage: "airbyte/destination-bigquery-denormalized:1.2.7"
834+
- dockerImage: "airbyte/destination-bigquery-denormalized:1.2.8"
835835
spec:
836836
documentationUrl: "https://docs.airbyte.com/integrations/destinations/bigquery"
837837
connectionSpecification:

airbyte-integrations/connectors/destination-bigquery-denormalized/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ ENV ENABLE_SENTRY true
1717

1818
COPY --from=build /airbyte /airbyte
1919

20-
LABEL io.airbyte.version=1.2.7
20+
LABEL io.airbyte.version=1.2.8
2121
LABEL io.airbyte.name=airbyte/destination-bigquery-denormalized

airbyte-integrations/connectors/destination-bigquery/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ ENV ENABLE_SENTRY true
1717

1818
COPY --from=build /airbyte /airbyte
1919

20-
LABEL io.airbyte.version=1.2.7
20+
LABEL io.airbyte.version=1.2.8
2121
LABEL io.airbyte.name=airbyte/destination-bigquery

airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java

+65-7
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
import com.fasterxml.jackson.databind.node.ObjectNode;
1212
import com.google.api.gax.rpc.HeaderProvider;
1313
import com.google.cloud.bigquery.BigQuery;
14+
import com.google.cloud.bigquery.BigQueryError;
1415
import com.google.cloud.bigquery.BigQueryException;
1516
import com.google.cloud.bigquery.Clustering;
1617
import com.google.cloud.bigquery.Dataset;
1718
import com.google.cloud.bigquery.DatasetInfo;
1819
import com.google.cloud.bigquery.Field;
1920
import com.google.cloud.bigquery.FieldList;
21+
import com.google.cloud.bigquery.InsertAllRequest;
22+
import com.google.cloud.bigquery.InsertAllResponse;
2023
import com.google.cloud.bigquery.Job;
2124
import com.google.cloud.bigquery.JobId;
2225
import com.google.cloud.bigquery.JobInfo;
@@ -25,12 +28,14 @@
2528
import com.google.cloud.bigquery.Schema;
2629
import com.google.cloud.bigquery.StandardSQLTypeName;
2730
import com.google.cloud.bigquery.StandardTableDefinition;
31+
import com.google.cloud.bigquery.Table;
2832
import com.google.cloud.bigquery.TableDefinition;
2933
import com.google.cloud.bigquery.TableId;
3034
import com.google.cloud.bigquery.TableInfo;
3135
import com.google.cloud.bigquery.TimePartitioning;
3236
import com.google.common.collect.ImmutableList;
3337
import com.google.common.collect.ImmutableMap;
38+
import io.airbyte.commons.exceptions.ConfigErrorException;
3439
import io.airbyte.commons.json.Jsons;
3540
import io.airbyte.config.WorkerEnvConstants;
3641
import io.airbyte.integrations.base.JavaBaseConstants;
@@ -45,6 +50,7 @@
4550
import java.time.format.DateTimeParseException;
4651
import java.util.ArrayList;
4752
import java.util.List;
53+
import java.util.Map;
4854
import java.util.Optional;
4955
import java.util.Set;
5056
import java.util.UUID;
@@ -64,7 +70,8 @@ public class BigQueryUtils {
6470
DateTimeFormatter.ofPattern("[yyyy][yy]['-']['/']['.'][' '][MMM][MM][M]['-']['/']['.'][' '][dd][d]" +
6571
"[[' ']['T']HH:mm[':'ss[.][SSSSSS][SSSSS][SSSS][SSS][' '][z][zzz][Z][O][x][XXX][XX][X]]]");
6672
private static final String USER_AGENT_FORMAT = "%s (GPN: Airbyte)";
67-
private static final String CHECK_TEST_DATASET_SUFFIX = "_airbyte_check_stage_tmp";
73+
private static final String CHECK_TEST_DATASET_SUFFIX = "_airbyte_check_stage_tmp_" + System.currentTimeMillis();
74+
private static final String CHECK_TEST_TMP_TABLE_NAME = "test_connection_table_name";
6875

6976
public static ImmutablePair<Job, String> executeQuery(final BigQuery bigquery, final QueryJobConfiguration queryConfig) {
7077
final JobId jobId = JobId.of(UUID.randomUUID().toString());
@@ -119,16 +126,67 @@ public static Dataset getOrCreateDataset(final BigQuery bigquery, final String d
119126

120127
public static void checkHasCreateAndDeleteDatasetRole(final BigQuery bigquery, final String datasetId, final String datasetLocation) {
121128
final String tmpTestDatasetId = datasetId + CHECK_TEST_DATASET_SUFFIX;
122-
final Dataset dataset = bigquery.getDataset(tmpTestDatasetId);
129+
final DatasetInfo datasetInfo = DatasetInfo.newBuilder(tmpTestDatasetId).setLocation(datasetLocation).build();
130+
131+
bigquery.create(datasetInfo);
123132

124-
// remove possible tmp datasets from previous execution
125-
if (dataset != null && dataset.exists()) {
133+
try {
134+
attemptCreateTableAndTestInsert(bigquery, tmpTestDatasetId);
135+
} finally {
126136
bigquery.delete(tmpTestDatasetId);
127137
}
138+
}
128139

129-
final DatasetInfo datasetInfo = DatasetInfo.newBuilder(tmpTestDatasetId).setLocation(datasetLocation).build();
130-
bigquery.create(datasetInfo);
131-
bigquery.delete(tmpTestDatasetId);
140+
/**
141+
* Method is used to create tmp table and make dummy record insert. It's used in Check() connection
142+
* method to make sure that user has all required roles for upcoming data sync/migration. It also
143+
* verifies if BigQuery project is billable, if not - later sync will fail as non-billable project
144+
* has limitations with stream uploading and DML queries. More details may be found there:
145+
* https://cloud.google.com/bigquery/docs/streaming-data-into-bigquery
146+
* https://cloud.google.com/bigquery/docs/reference/standard-sql/data-manipulation-language
147+
*
148+
* @param bigquery - initialized bigquery client
149+
* @param tmpTestDatasetId - dataset name where tmp table will be created
150+
*/
151+
private static void attemptCreateTableAndTestInsert(final BigQuery bigquery, final String tmpTestDatasetId) {
152+
// Create dummy schema that will be used for tmp table creation
153+
final Schema testTableSchema = Schema.of(
154+
Field.of("id", StandardSQLTypeName.INT64),
155+
Field.of("name", StandardSQLTypeName.STRING));
156+
157+
// Create tmp table to verify if user has a create table permission. Also below we will do test
158+
// records insert in it
159+
final Table test_connection_table_name = createTable(bigquery, tmpTestDatasetId,
160+
CHECK_TEST_TMP_TABLE_NAME, testTableSchema);
161+
162+
// Try to make test (dummy records) insert to make sure that user has required permissions
163+
try {
164+
final InsertAllResponse response =
165+
bigquery.insertAll(InsertAllRequest
166+
.newBuilder(test_connection_table_name)
167+
.addRow(Map.of("id", 1, "name", "James"))
168+
.addRow(Map.of("id", 2, "name", "Eugene"))
169+
.addRow(Map.of("id", 3, "name", "Angelina"))
170+
.build());
171+
172+
if (response.hasErrors()) {
173+
// If any of the insertions failed, this lets you inspect the errors
174+
for (Map.Entry<Long, List<BigQueryError>> entry : response.getInsertErrors().entrySet()) {
175+
throw new ConfigErrorException("Failed to check connection: \n" + entry.getValue());
176+
}
177+
}
178+
} catch (final BigQueryException e) {
179+
throw new ConfigErrorException("Failed to check connection: \n" + e.getMessage());
180+
} finally {
181+
test_connection_table_name.delete();
182+
}
183+
}
184+
185+
public static Table createTable(final BigQuery bigquery, String datasetName, String tableName, Schema schema) {
186+
final TableId tableId = TableId.of(datasetName, tableName);
187+
final TableDefinition tableDefinition = StandardTableDefinition.of(schema);
188+
final TableInfo tableInfo = TableInfo.newBuilder(tableId, tableDefinition).build();
189+
return bigquery.create(tableInfo);
132190
}
133191

134192
// https://cloud.google.com/bigquery/docs/creating-partitioned-tables#java

airbyte-integrations/connectors/destination-bigquery/src/test-integration/java/io/airbyte/integrations/destination/bigquery/BigQueryDestinationTest.java

+38-2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class BigQueryDestinationTest {
7676
protected static final Path CREDENTIALS_PATH = Path.of("secrets/credentials.json");
7777
protected static final Path CREDENTIALS_WITH_MISSED_CREATE_DATASET_ROLE_PATH =
7878
Path.of("secrets/credentials-with-missed-dataset-creation-role.json");
79+
protected static final Path CREDENTIALS_NON_BILLABLE_PROJECT_PATH =
80+
Path.of("secrets/credentials-non-billable-project.json");
7981

8082
private static final Logger LOGGER = LoggerFactory.getLogger(BigQueryDestinationTest.class);
8183
private static final String DATASET_NAME_PREFIX = "bq_dest_integration_test";
@@ -250,8 +252,7 @@ void testCheckFailureInsufficientPermissionForCreateDataset(final DatasetIdReset
250252
please add file with creds to
251253
../destination-bigquery/secrets/credentialsWithMissedDatasetCreationRole.json.""");
252254
}
253-
final String fullConfigAsString = Files.readString(
254-
CREDENTIALS_WITH_MISSED_CREATE_DATASET_ROLE_PATH);
255+
final String fullConfigAsString = Files.readString(CREDENTIALS_WITH_MISSED_CREATE_DATASET_ROLE_PATH);
255256
final JsonNode credentialsJson = Jsons.deserialize(fullConfigAsString).get(BigQueryConsts.BIGQUERY_BASIC_CONFIG);
256257
final String projectId = credentialsJson.get(BigQueryConsts.CONFIG_PROJECT_ID).asText();
257258
final String datasetId = Strings.addRandomSuffix(DATASET_NAME_PREFIX, "_", 8);
@@ -276,6 +277,41 @@ void testCheckFailureInsufficientPermissionForCreateDataset(final DatasetIdReset
276277
assertThat(ex.getMessage()).contains("User does not have bigquery.datasets.create permission");
277278
}
278279

280+
@ParameterizedTest
281+
@MethodSource("datasetIdResetterProvider")
282+
void testCheckFailureNonBillableProject(final DatasetIdResetter resetDatasetId) throws IOException {
283+
284+
if (!Files.exists(CREDENTIALS_NON_BILLABLE_PROJECT_PATH)) {
285+
throw new IllegalStateException("""
286+
Json config not found. Must provide path to a big query credentials file,
287+
please add file with creds to
288+
../destination-bigquery/secrets/credentials-non-billable-project.json""");
289+
}
290+
final String fullConfigAsString = Files.readString(CREDENTIALS_NON_BILLABLE_PROJECT_PATH);
291+
292+
final JsonNode credentialsJson = Jsons.deserialize(fullConfigAsString).get(BigQueryConsts.BIGQUERY_BASIC_CONFIG);
293+
final String projectId = credentialsJson.get(BigQueryConsts.CONFIG_PROJECT_ID).asText();
294+
295+
final JsonNode insufficientRoleConfig;
296+
297+
insufficientRoleConfig = Jsons.jsonNode(ImmutableMap.builder()
298+
.put(BigQueryConsts.CONFIG_PROJECT_ID, projectId)
299+
.put(BigQueryConsts.CONFIG_CREDS, credentialsJson.toString())
300+
.put(BigQueryConsts.CONFIG_DATASET_ID, "testnobilling")
301+
.put(BigQueryConsts.CONFIG_DATASET_LOCATION, "US")
302+
.put(BIG_QUERY_CLIENT_CHUNK_SIZE, 10)
303+
.build());
304+
305+
resetDatasetId.accept(insufficientRoleConfig);
306+
307+
// Assert that check throws exception. Later it will be handled by IntegrationRunner
308+
final ConfigErrorException ex = assertThrows(ConfigErrorException.class, () -> {
309+
new BigQueryDestination().check(insufficientRoleConfig);
310+
});
311+
312+
assertThat(ex.getMessage()).contains("Access Denied: BigQuery BigQuery: Streaming insert is not allowed in the free tier");
313+
}
314+
279315
@ParameterizedTest
280316
@MethodSource("datasetIdResetterProvider")
281317
void testWriteSuccess(final DatasetIdResetter resetDatasetId) throws Exception {

docs/integrations/destinations/bigquery.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ Now that you have set up the BigQuery destination connector, check out the follo
136136

137137
| Version | Date | Pull Request | Subject |
138138
|:--------|:-----------|:----------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------|
139-
| 1.2.7 | 2022-11-11 | [#19358](https://github.com/airbytehq/airbyte/pull/19358) | fixed check method to capture mismatch dataset location |
139+
| 1.2.8 | 2022-11-22 | [#19489](https://github.com/airbytehq/airbyte/pull/19489) | Added non-billable projects handle to check connection stage |
140+
| 1.2.7 | 2022-11-11 | [#19358](https://github.com/airbytehq/airbyte/pull/19358) | Fixed check method to capture mismatch dataset location |
140141
| 1.2.6 | 2022-11-10 | [#18554](https://github.com/airbytehq/airbyte/pull/18554) | Improve check connection method to handle more errors |
141142
| 1.2.5 | 2022-10-19 | [#18162](https://github.com/airbytehq/airbyte/pull/18162) | Improve error logs |
142143
| 1.2.4 | 2022-09-26 | [#16890](https://github.com/airbytehq/airbyte/pull/16890) | Add user-agent header |
@@ -189,7 +190,8 @@ Now that you have set up the BigQuery destination connector, check out the follo
189190

190191
| Version | Date | Pull Request | Subject |
191192
|:--------|:-----------|:----------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------|
192-
| 1.2.7 | 2022-11-11 | [#19358](https://github.com/airbytehq/airbyte/pull/19358) | fixed check method to capture mismatch dataset location |
193+
| 1.2.8 | 2022-11-22 | [#19489](https://github.com/airbytehq/airbyte/pull/19489) | Added non-billable projects handle to check connection stage |
194+
| 1.2.7 | 2022-11-11 | [#19358](https://github.com/airbytehq/airbyte/pull/19358) | Fixed check method to capture mismatch dataset location |
193195
| 1.2.6 | 2022-11-10 | [#18554](https://github.com/airbytehq/airbyte/pull/18554) | Improve check connection method to handle more errors |
194196
| 1.2.5 | 2022-10-19 | [#18162](https://github.com/airbytehq/airbyte/pull/18162) | Improve error logs |
195197
| 1.2.4 | 2022-09-26 | [#16890](https://github.com/airbytehq/airbyte/pull/16890) | Add user-agent header |

0 commit comments

Comments
 (0)