blob: ac965e8179ddab12fd415e496545e84d643dbc97 [file] [log] [blame]
package build.bazel.dashboard.github.issuelist;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Objects.requireNonNull;
import build.bazel.dashboard.github.issuelist.GithubIssueListService.ListParams;
import build.bazel.dashboard.github.issuestatus.GithubIssueStatus;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import io.r2dbc.postgresql.codec.Json;
import io.r2dbc.postgresql.codec.PostgresqlObjectId;
import io.r2dbc.spi.Parameters;
import io.r2dbc.spi.Readable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.DatabaseClient.GenericExecuteSpec;
import org.springframework.stereotype.Repository;
import reactor.adapter.rxjava.RxJava3Adapter;
@Repository
@RequiredArgsConstructor
public class GithubIssueListRepoPg implements GithubIssueListRepo {
private final ObjectMapper objectMapper;
private final DatabaseClient databaseClient;
@Builder
static class QuerySpec {
String from;
String where;
String order;
String limit;
Map<String, Object> bindings;
}
private static QuerySpec buildQuerySpec(ListParams params) {
String from =
" FROM github_issue_status gis INNER JOIN github_issue gi ON (gi.owner, gi.repo,"
+ " gi.issue_number) = (gis.owner, gis.repo, gis.issue_number) INNER JOIN"
+ " github_issue_data gid ON (gid.owner, gid.repo, gid.issue_number) = (gi.owner,"
+ " gi.repo, gi.issue_number)";
Map<String, Object> bindings = new HashMap<>();
StringBuilder where = new StringBuilder();
if (params.getOwner() != null) {
where.append(" AND gis.owner = :owner");
bindings.put("owner", params.getOwner());
}
if (params.getRepo() != null) {
where.append(" AND gis.repo = :repo");
bindings.put("repo", params.getRepo());
}
if (params.getIsPullRequest() != null) {
where.append(" AND gi.is_pull_request = :is_pull_request");
bindings.put("is_pull_request", params.getIsPullRequest());
}
if (params.getStatus() != null) {
where.append(" AND gis.status = :status");
bindings.put("status", params.getStatus().toString());
}
if (params.getActionOwner() != null) {
where.append(
" AND (gis.action_owner = :action_owner OR :action_owner = ANY(gis.more_action_owners))");
bindings.put("action_owner", params.getActionOwner());
}
if (params.getLabels() != null && !params.getLabels().isEmpty()) {
where.append(" AND gi.labels @> :labels");
bindings.put(
"labels",
Parameters.in(PostgresqlObjectId.UNSPECIFIED, params.getLabels().toArray(new String[0])));
}
if (where.length() > 0) {
// Remove heading AND
where.delete(0, 4);
where.insert(0, " WHERE");
}
String order = " ORDER BY gi.issue_number DESC";
if (params.getSort() != null) {
switch (params.getSort()) {
case EXPECTED_RESPOND_AT_ASC:
order = " ORDER BY gis.expected_respond_at ASC";
break;
case EXPECTED_RESPOND_AT_DESC:
order = " ORDER BY gis.expected_respond_at DESC";
break;
}
}
int page = requireNonNull(params.getPage());
int pageSize = requireNonNull(params.getPageSize());
int offset = (page - 1) * pageSize;
String limit = " LIMIT " + pageSize + " OFFSET " + offset;
return QuerySpec.builder()
.from(from)
.where(where.toString())
.order(order)
.limit(limit)
.bindings(bindings)
.build();
}
@Override
public Flowable<GithubIssueList.Item> find(ListParams params) {
QuerySpec query = buildQuerySpec(params);
String fields =
"gid.owner, gid.repo, gid.issue_number, gis.status, gis.action_owner,"
+ " gis.more_action_owners, gis.expected_respond_at, gid.data";
String sql = "SELECT " + fields + query.from + query.where + query.order + query.limit;
GenericExecuteSpec spec = databaseClient.sql(sql);
for (Map.Entry<String, Object> binding : query.bindings.entrySet()) {
spec = spec.bind(binding.getKey(), binding.getValue());
}
return RxJava3Adapter.fluxToFlowable(spec.map(this::toGithubIssueListItem).all());
}
@Override
public Single<Integer> count(ListParams params) {
QuerySpec query = buildQuerySpec(params);
String sql = "SELECT COUNT(*) as total" + query.from + query.where;
GenericExecuteSpec spec = databaseClient.sql(sql);
for (Map.Entry<String, Object> binding : query.bindings.entrySet()) {
spec = spec.bind(binding.getKey(), binding.getValue());
}
return RxJava3Adapter.monoToSingle(spec.map(row -> row.get("total", Integer.class)).one());
}
private GithubIssueList.Item toGithubIssueListItem(Readable row) {
String actionOwner = row.get("action_owner", String.class);
if (actionOwner == null) {
String[] moreActionOwners = requireNonNull(row.get("more_action_owners", String[].class));
if (moreActionOwners.length > 0) {
actionOwner = moreActionOwners[0];
}
}
try {
return GithubIssueList.Item.builder()
.owner(requireNonNull(row.get("owner", String.class)))
.repo(requireNonNull(row.get("repo", String.class)))
.issueNumber(requireNonNull(row.get("issue_number", Integer.class)))
.status(GithubIssueStatus.Status.valueOf(row.get("status", String.class)))
.actionOwner(actionOwner)
.expectedRespondAt(row.get("expected_respond_at", Instant.class))
.data(objectMapper.readTree((requireNonNull(row.get("data", Json.class))).asArray()))
.build();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
@Override
public ImmutableList<String> findAllActionOwner(ListParams params) {
if (params.getActionOwner() != null) {
return ImmutableList.of(params.getActionOwner());
}
QuerySpec query = buildQuerySpec(params);
StringBuilder sql =
new StringBuilder(
"SELECT DISTINCT unnest(array_append(gis.more_action_owners, gis.action_owner)) as"
+ " action_owner");
sql.append(query.from);
sql.append(query.where);
GenericExecuteSpec spec = databaseClient.sql(sql.toString());
for (Map.Entry<String, Object> binding : query.bindings.entrySet()) {
spec = spec.bind(binding.getKey(), binding.getValue());
}
return spec.map(
row -> {
String actionOwner = row.get("action_owner", String.class);
if (actionOwner == null) {
actionOwner = "";
}
return actionOwner;
})
.all()
.collect(toImmutableList())
.block();
}
@Override
public ImmutableList<String> findAllLabels(ListParams params) {
QuerySpec query = buildQuerySpec(params);
StringBuilder sql = new StringBuilder("SELECT DISTINCT unnest(gi.labels) AS label");
sql.append(query.from);
sql.append(query.where);
GenericExecuteSpec spec = databaseClient.sql(sql.toString());
for (Map.Entry<String, Object> binding : query.bindings.entrySet()) {
spec = spec.bind(binding.getKey(), binding.getValue());
}
return spec.map(row -> requireNonNull(row.get("label", String.class)))
.all()
.collect(toImmutableList())
.block();
}
}