blob: 795caaa4382e217a9d7a1613a8db3748312cf876 [file] [log] [blame]
// Copyright 2022 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:auto_submit/exception/bigquery_exception.dart';
import 'package:auto_submit/model/big_query_pull_request_record.dart';
import 'package:auto_submit/model/big_query_revert_request_record.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:http/http.dart';
import 'access_client_provider.dart';
const String selectRevertRequestDml = r'''
SELECT organization,
repository,
reverting_pr_author,
reverting_pr_number,
reverting_pr_commit,
reverting_pr_created_timestamp,
reverting_pr_landed_timestamp,
original_pr_author,
original_pr_number,
original_pr_commit,
original_pr_created_timestamp,
original_pr_landed_timestamp,
review_issue_assignee,
review_issue_number,
review_issue_created_timestamp,
review_issue_landed_timestamp,
review_issue_closed_by
FROM `flutter-dashboard.revert.revert_requests`
WHERE reverting_pr_number=@REVERTING_PR_NUMBER AND repository=@REPOSITORY
''';
/// Query to select all of the open revert review issues for update. Note that
/// the review_issue_landed_timestamp is 0 for open issues. 0 is the default
/// value instead of null since it is safer to process.
const String selectRevertRequestReviewIssuesDml = r'''
SELECT review_issue_assignee,
review_issue_number,
review_issue_created_timestamp,
review_issue_landed_timestamp,
review_issue_closed_by
FROM `flutter-dashboard.revert.revert_requests`
WHERE review_issue_landed_timestamp=0
''';
const String updateRevertRequestRecordReviewDml = '''
UPDATE `flutter-dashboard.revert.revert_requests`
SET review_issue_landed_timestamp=@REVIEW_ISSUE_LANDED_TIMESTAMP,
review_issue_closed_by=@REVIEW_ISSUE_CLOSED_BY
WHERE review_issue_number=@REVIEW_ISSUE_NUMBER
''';
const String insertRevertRequestDml = r'''
INSERT INTO `flutter-dashboard.revert.revert_requests` (
organization,
repository,
reverting_pr_author,
reverting_pr_number,
reverting_pr_commit,
reverting_pr_created_timestamp,
reverting_pr_landed_timestamp,
original_pr_author,
original_pr_number,
original_pr_commit,
original_pr_created_timestamp,
original_pr_landed_timestamp,
review_issue_assignee,
review_issue_number,
review_issue_created_timestamp,
review_issue_landed_timestamp,
review_issue_closed_by
) VALUES (
@ORGANIZATION,
@REPOSITORY,
@REVERTING_PR_AUTHOR,
@REVERTING_PR_NUMBER,
@REVERTING_PR_COMMIT,
@REVERTING_PR_CREATED_TIMESTAMP,
@REVERTING_PR_LANDED_TIMESTAMP,
@ORIGINAL_PR_AUTHOR,
@ORIGINAL_PR_NUMBER,
@ORIGINAL_PR_COMMIT,
@ORIGINAL_PR_CREATED_TIMESTAMP,
@ORIGINAL_PR_LANDED_TIMESTAMP,
@REVIEW_ISSUE_ASSIGNEE,
@REVIEW_ISSUE_NUMBER,
@REVIEW_ISSUE_CREATED_TIMESTAMP,
@REVIEW_ISSUE_LANDED_TIMESTAMP,
@REVIEW_ISSUE_CLOSED_BY
)
''';
const String deleteRevertRequestDml = r'''
DELETE FROM `flutter-dashboard.revert.revert_requests`
WHERE reverting_pr_number=@REVERTING_PR_NUMBER AND repository=@REPOSITORY
''';
const String insertPullRequestDml = r'''
INSERT INTO `flutter-dashboard.autosubmit.pull_requests` (
pr_created_timestamp,
pr_landed_timestamp,
organization,
repository,
author,
pr_number,
pr_commit,
pr_request_type
) VALUES (
@PR_CREATED_TIMESTAMP,
@PR_LANDED_TIMESTAMP,
@ORGANIZATION,
@REPOSITORY,
@AUTHOR,
@PR_NUMBER,
@PR_COMMIT,
@PR_REQUEST_TYPE
)
''';
const String selectPullRequestDml = r'''
SELECT pr_created_timestamp,
pr_landed_timestamp,
organization,
repository,
author,
pr_number,
pr_commit,
pr_request_type
FROM `flutter-dashboard.autosubmit.pull_requests`
WHERE pr_number=@PR_NUMBER AND repository=@REPOSITORY
''';
const String deletePullRequestDml = r'''
DELETE FROM `flutter-dashboard.autosubmit.pull_requests`
WHERE pr_number=@PR_NUMBER AND repository=@REPOSITORY
''';
class BigqueryService {
const BigqueryService(this.accessClientProvider);
/// AccessClientProvider for OAuth 2.0 authenticated access client
final AccessClientProvider accessClientProvider;
/// Return a [TabledataResource] with an authenticated [client]
Future<TabledataResource> defaultTabledata() async {
final Client client = await accessClientProvider.createAccessClient(
scopes: const <String>[BigqueryApi.bigqueryScope],
);
return BigqueryApi(client).tabledata;
}
/// Return a [JobsResource] with an authenticated [client]
Future<JobsResource> defaultJobs() async {
final Client client = await accessClientProvider.createAccessClient(
scopes: const <String>[BigqueryApi.bigqueryScope],
);
return BigqueryApi(client).jobs;
}
/// Insert a new revert request into the database.
Future<void> insertRevertRequestRecord({
required String projectId,
required RevertRequestRecord revertRequestRecord,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: insertRevertRequestDml,
queryParameters: <QueryParameter>[
_createStringQueryParameter(
'ORGANIZATION',
revertRequestRecord.organization,
),
_createStringQueryParameter(
'REPOSITORY',
revertRequestRecord.repository,
),
_createStringQueryParameter(
'REVERTING_PR_AUTHOR',
revertRequestRecord.author,
),
_createIntegerQueryParameter(
'REVERTING_PR_NUMBER',
revertRequestRecord.prNumber!,
),
_createStringQueryParameter(
'REVERTING_PR_COMMIT',
revertRequestRecord.prCommit,
),
_createIntegerQueryParameter(
'REVERTING_PR_CREATED_TIMESTAMP',
revertRequestRecord.prCreatedTimestamp!.millisecondsSinceEpoch,
),
_createIntegerQueryParameter(
'REVERTING_PR_LANDED_TIMESTAMP',
revertRequestRecord.prLandedTimestamp!.millisecondsSinceEpoch,
),
_createStringQueryParameter(
'ORIGINAL_PR_AUTHOR',
revertRequestRecord.originalPrAuthor,
),
_createIntegerQueryParameter(
'ORIGINAL_PR_NUMBER',
revertRequestRecord.originalPrNumber!,
),
_createStringQueryParameter(
'ORIGINAL_PR_COMMIT',
revertRequestRecord.originalPrCommit,
),
_createIntegerQueryParameter(
'ORIGINAL_PR_CREATED_TIMESTAMP',
revertRequestRecord.originalPrCreatedTimestamp!.millisecondsSinceEpoch,
),
_createIntegerQueryParameter(
'ORIGINAL_PR_LANDED_TIMESTAMP',
revertRequestRecord.originalPrLandedTimestamp!.millisecondsSinceEpoch,
),
_createStringQueryParameter(
'REVIEW_ISSUE_ASSIGNEE',
revertRequestRecord.reviewIssueAssignee,
),
_createIntegerQueryParameter(
'REVIEW_ISSUE_NUMBER',
revertRequestRecord.reviewIssueNumber,
),
_createIntegerQueryParameter(
'REVIEW_ISSUE_CREATED_TIMESTAMP',
revertRequestRecord.reviewIssueCreatedTimestamp!.millisecondsSinceEpoch,
),
// This could not possibly be landed at the time of entry into the database but we should check for null.
_createIntegerQueryParameter(
'REVIEW_ISSUE_LANDED_TIMESTAMP',
(revertRequestRecord.reviewIssueLandedTimestamp != null)
? revertRequestRecord.reviewIssueLandedTimestamp!.millisecondsSinceEpoch
: 0,
),
_createStringQueryParameter(
'REVIEW_ISSUE_CLOSED_BY',
'',
),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Insert revert request $revertRequestRecord did not complete.',
);
}
if (queryResponse.numDmlAffectedRows != null && int.parse(queryResponse.numDmlAffectedRows!) != 1) {
throw BigQueryException(
'There was an error inserting $revertRequestRecord into the table.',
);
}
}
/// Select a specific revert request from the database.
Future<RevertRequestRecord> selectRevertRequestByRevertPrNumber({
required String projectId,
required int prNumber,
required String repository,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: selectRevertRequestDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter('REVERTING_PR_NUMBER', prNumber),
_createStringQueryParameter('REPOSITORY', repository),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Get revert request with pr# $prNumber in repository $repository did not complete.',
);
}
final List<TableRow>? tableRows = queryResponse.rows;
if (tableRows == null || tableRows.isEmpty) {
throw BigQueryException(
'Could not find an entry for revert request with pr# $prNumber in repository $repository.',
);
}
if (tableRows.length != 1) {
throw BigQueryException(
'More than one record was returned for revert request with pr# $prNumber in repository $repository.',
);
}
final TableRow tableRow = tableRows.first;
return RevertRequestRecord(
organization: tableRow.f![0].v as String,
repository: tableRow.f![1].v as String,
author: tableRow.f![2].v as String,
prNumber: int.parse(tableRow.f![3].v as String),
prCommit: tableRow.f![4].v as String,
prCreatedTimestamp: (tableRow.f![5].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![5].v as String))
: null,
prLandedTimestamp: (tableRow.f![6].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![6].v as String))
: null,
originalPrAuthor: tableRow.f![7].v as String,
originalPrNumber: int.parse(tableRow.f![8].v as String),
originalPrCommit: tableRow.f![9].v as String,
originalPrCreatedTimestamp: (tableRow.f![10].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![10].v as String))
: null,
originalPrLandedTimestamp: (tableRow.f![11].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![11].v as String))
: null,
reviewIssueAssignee: tableRow.f![12].v as String,
reviewIssueNumber: int.parse(tableRow.f![13].v as String),
reviewIssueCreatedTimestamp: (tableRow.f![14].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![14].v as String))
: null,
reviewIssueLandedTimestamp: (tableRow.f![15].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![15].v as String))
: null,
reviewIssueClosedBy: tableRow.f![16].v as String,
);
}
/// Query to select the open review issues for update.
///
/// Issues are open if the review_issue_landed_timestamp is equal to 0.
Future<List<RevertRequestRecord>> selectOpenReviewRequestIssueRecordsList({
required String projectId,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: selectRevertRequestReviewIssuesDml,
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Get open review request issues records did not complete.',
);
}
final List<TableRow>? tableRows = queryResponse.rows;
if (tableRows == null || tableRows.isEmpty) {
return <RevertRequestRecord>[];
}
List<RevertRequestRecord> openReviewRequestIssues = [];
for (TableRow tableRow in tableRows) {
openReviewRequestIssues.add(
RevertRequestRecord(
reviewIssueAssignee: tableRow.f![0].v as String,
reviewIssueNumber: int.parse(tableRow.f![1].v as String),
reviewIssueCreatedTimestamp: DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![2].v as String)),
reviewIssueLandedTimestamp: DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![3].v as String)),
reviewIssueClosedBy: tableRow.f![4].v as String,
),
);
}
return openReviewRequestIssues;
}
/// Query the database to update a revert review issue with the timestamp the
/// issue was closed at and the person who closed the issue.
Future<void> updateReviewRequestIssue({
required String projectId,
required DateTime reviewIssueLandedTimestamp,
required int reviewIssueNumber,
required String reviewIssueClosedBy,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: updateRevertRequestRecordReviewDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter(
'REVIEW_ISSUE_LANDED_TIMESTAMP',
reviewIssueLandedTimestamp.millisecondsSinceEpoch,
),
_createIntegerQueryParameter(
'REVIEW_ISSUE_NUMBER',
reviewIssueNumber,
),
_createStringQueryParameter(
'REVIEW_ISSUE_CLOSED_BY',
reviewIssueClosedBy,
),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Update of review issue $reviewIssueNumber did not complete.',
);
}
if (queryResponse.numDmlAffectedRows != null && int.parse(queryResponse.numDmlAffectedRows!) != 1) {
throw BigQueryException(
'There was an error updating revert request record review issue landed timestamp with review issue number $reviewIssueNumber.',
);
}
}
Future<void> deleteRevertRequestRecord({
required String projectId,
required int prNumber,
required String repository,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: deleteRevertRequestDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter('REVERTING_PR_NUMBER', prNumber),
_createStringQueryParameter('REPOSITORY', repository),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Delete revert request with pr# $prNumber in repository $repository did not complete.',
);
}
if (queryResponse.numDmlAffectedRows == null || int.parse(queryResponse.numDmlAffectedRows!) == 0) {
throw BigQueryException(
'Could not find revert request with pr# $prNumber in repository $repository to delete.',
);
}
if (int.parse(queryResponse.numDmlAffectedRows!) != 1) {
throw BigQueryException(
'More than one row was deleted from the database for revert request with pr# $prNumber in repository $repository.',
);
}
}
/// Insert a new pull request record into the database.
Future<void> insertPullRequestRecord({
required String projectId,
required PullRequestRecord pullRequestRecord,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: insertPullRequestDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter(
'PR_CREATED_TIMESTAMP',
pullRequestRecord.prCreatedTimestamp!.millisecondsSinceEpoch,
),
_createIntegerQueryParameter(
'PR_LANDED_TIMESTAMP',
pullRequestRecord.prLandedTimestamp!.millisecondsSinceEpoch,
),
_createStringQueryParameter(
'ORGANIZATION',
pullRequestRecord.organization,
),
_createStringQueryParameter(
'REPOSITORY',
pullRequestRecord.repository,
),
_createStringQueryParameter(
'AUTHOR',
pullRequestRecord.author,
),
_createIntegerQueryParameter(
'PR_NUMBER',
pullRequestRecord.prNumber,
),
_createStringQueryParameter(
'PR_COMMIT',
pullRequestRecord.prCommit,
),
_createStringQueryParameter(
'PR_REQUEST_TYPE',
pullRequestRecord.prRequestType,
),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Insert pull request $pullRequestRecord did not complete.',
);
}
if (queryResponse.numDmlAffectedRows != null && int.parse(queryResponse.numDmlAffectedRows!) != 1) {
throw BigQueryException(
'There was an error inserting $pullRequestRecord into the table.',
);
}
}
/// Select a specific pull request form the database.
Future<PullRequestRecord> selectPullRequestRecordByPrNumber({
required String projectId,
required int prNumber,
required String repository,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: selectPullRequestDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter('PR_NUMBER', prNumber),
_createStringQueryParameter('REPOSITORY', repository),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Get pull request by pr# $prNumber in repository $repository did not complete.',
);
}
final List<TableRow>? tableRows = queryResponse.rows;
if (tableRows == null || tableRows.isEmpty) {
throw BigQueryException(
'Could not find an entry for pull request with pr# $prNumber in repository $repository.',
);
}
if (tableRows.length != 1) {
throw BigQueryException(
'More than one record was returned for pull request with pr# $prNumber in repository $repository.',
);
}
final TableRow tableRow = tableRows.first;
return PullRequestRecord(
prCreatedTimestamp: (tableRow.f![0].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![0].v as String))
: null,
prLandedTimestamp: (tableRow.f![1].v != null)
? DateTime.fromMillisecondsSinceEpoch(int.parse(tableRow.f![1].v as String))
: null,
organization: tableRow.f![2].v as String,
repository: tableRow.f![3].v as String,
author: tableRow.f![4].v as String,
prNumber: int.parse(tableRow.f![5].v as String),
prCommit: tableRow.f![6].v as String,
prRequestType: tableRow.f![7].v as String,
);
}
Future<void> deletePullRequestRecord({
required String projectId,
required int prNumber,
required String repository,
}) async {
final JobsResource jobsResource = await defaultJobs();
final QueryRequest queryRequest = QueryRequest(
query: deletePullRequestDml,
queryParameters: <QueryParameter>[
_createIntegerQueryParameter('PR_NUMBER', prNumber),
_createStringQueryParameter('REPOSITORY', repository),
],
useLegacySql: false,
);
final QueryResponse queryResponse = await jobsResource.query(queryRequest, projectId);
if (!queryResponse.jobComplete!) {
throw BigQueryException(
'Delete pull request with pr# $prNumber in repository $repository did not complete.',
);
}
if (queryResponse.numDmlAffectedRows == null || int.parse(queryResponse.numDmlAffectedRows!) == 0) {
throw BigQueryException(
'Could not find pull request with pr# $prNumber in repository $repository to delete.',
);
}
if (int.parse(queryResponse.numDmlAffectedRows!) != 1) {
throw BigQueryException(
'More than one row was deleted from the database for pull request with pr# $prNumber in repository $repository.',
);
}
}
/// Create an int parameter for query substitution.
QueryParameter _createIntegerQueryParameter(String name, int? value) {
return QueryParameter(
name: name,
parameterType: QueryParameterType(type: 'INT64'),
parameterValue: QueryParameterValue(value: value.toString()),
);
}
/// Create a String parameter for query substitution.
QueryParameter _createStringQueryParameter(String name, String? value) {
return QueryParameter(
name: name,
parameterType: QueryParameterType(type: 'STRING'),
parameterValue: QueryParameterValue(value: value),
);
}
}