com.scalar.db.storage.cosmos.SelectStatementHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scalardb Show documentation
Show all versions of scalardb Show documentation
A universal transaction manager that achieves database-agnostic transactions and distributed transactions that span multiple databases
package com.scalar.db.storage.cosmos;
import static com.scalar.db.storage.cosmos.CosmosUtils.quoteKeyword;
import com.azure.cosmos.CosmosClient;
import com.azure.cosmos.CosmosException;
import com.azure.cosmos.models.CosmosQueryRequestOptions;
import com.azure.cosmos.models.FeedResponse;
import com.azure.cosmos.models.PartitionKey;
import com.scalar.db.api.Get;
import com.scalar.db.api.Scan;
import com.scalar.db.api.Scan.Ordering.Order;
import com.scalar.db.api.ScanAll;
import com.scalar.db.api.Scanner;
import com.scalar.db.api.Selection;
import com.scalar.db.api.TableMetadata;
import com.scalar.db.common.EmptyScanner;
import com.scalar.db.common.TableMetadataManager;
import com.scalar.db.exception.storage.ExecutionException;
import com.scalar.db.io.Column;
import com.scalar.db.util.ScalarDbUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import org.jooq.Field;
import org.jooq.SQLDialect;
import org.jooq.SelectConditionStep;
import org.jooq.SelectJoinStep;
import org.jooq.SelectWhereStep;
import org.jooq.conf.ParamType;
import org.jooq.impl.DSL;
/**
* A handler class for select statements
*
* @author Yuji Ito
*/
@ThreadSafe
public class SelectStatementHandler extends StatementHandler {
public SelectStatementHandler(CosmosClient client, TableMetadataManager metadataManager) {
super(client, metadataManager);
}
/**
* Executes the specified {@code Selection}
*
* @param selection a {@code Selection} to execute
* @return a {@code Scanner}
* @throws ExecutionException if the execution fails
*/
@Nonnull
protected Scanner handle(Selection selection) throws ExecutionException {
TableMetadata tableMetadata = metadataManager.getTableMetadata(selection);
try {
if (selection instanceof Get) {
return executeRead((Get) selection, tableMetadata);
} else {
return executeQuery((Scan) selection, tableMetadata);
}
} catch (CosmosException e) {
if (e.getStatusCode() == CosmosErrorCode.NOT_FOUND.get()) {
return new EmptyScanner();
}
throw new ExecutionException(e.getMessage(), e);
} catch (RuntimeException e) {
throw new ExecutionException(e.getMessage(), e);
}
}
private Scanner executeRead(Get get, TableMetadata tableMetadata) throws CosmosException {
CosmosOperation cosmosOperation = new CosmosOperation(get, tableMetadata);
cosmosOperation.checkArgument(Get.class);
if (ScalarDbUtils.isSecondaryIndexSpecified(get, tableMetadata)) {
return executeReadWithIndex(get, tableMetadata);
}
if (get.getProjections().isEmpty()) {
String id = cosmosOperation.getId();
PartitionKey partitionKey = cosmosOperation.getCosmosPartitionKey();
Record record = getContainer(get).readItem(id, partitionKey, Record.class).getItem();
return new SingleRecordScanner(
record, new ResultInterpreter(get.getProjections(), tableMetadata));
}
String query =
makeQueryWithProjections(get, tableMetadata)
.where(
DSL.field("r.concatenatedPartitionKey")
.eq(cosmosOperation.getConcatenatedPartitionKey()),
DSL.field("r.id").eq(cosmosOperation.getId()))
.getSQL(ParamType.INLINED);
return executeQuery(get, tableMetadata, query);
}
private Scanner executeReadWithIndex(Selection selection, TableMetadata tableMetadata)
throws CosmosException {
String query = makeQueryWithIndex(selection, tableMetadata);
return executeQuery(selection, tableMetadata, query);
}
private Scanner executeQuery(Scan scan, TableMetadata tableMetadata) throws CosmosException {
CosmosOperation cosmosOperation = new CosmosOperation(scan, tableMetadata);
String query;
CosmosQueryRequestOptions options;
if (scan instanceof ScanAll) {
query = makeQueryWithProjections(scan, tableMetadata).getSQL(ParamType.INLINED);
options = new CosmosQueryRequestOptions();
} else if (ScalarDbUtils.isSecondaryIndexSpecified(scan, tableMetadata)) {
query = makeQueryWithIndex(scan, tableMetadata);
options = new CosmosQueryRequestOptions();
} else {
query = makeQueryWithCondition(tableMetadata, cosmosOperation, scan);
options =
new CosmosQueryRequestOptions().setPartitionKey(cosmosOperation.getCosmosPartitionKey());
}
if (scan.getLimit() > 0) {
// Add limit as a string
// because JOOQ doesn't support OFFSET LIMIT clause which Cosmos DB requires
query += " offset 0 limit " + scan.getLimit();
}
return executeQuery(scan, tableMetadata, query, options);
}
private String makeQueryWithCondition(
TableMetadata tableMetadata, CosmosOperation cosmosOperation, Scan scan) {
String concatenatedPartitionKey = cosmosOperation.getConcatenatedPartitionKey();
SelectConditionStep select =
makeQueryWithProjections(scan, tableMetadata)
.where(DSL.field("r.concatenatedPartitionKey").eq(concatenatedPartitionKey));
setStart(select, scan);
setEnd(select, scan);
setOrderings(select, scan.getOrderings(), tableMetadata);
return select.getSQL(ParamType.INLINED);
}
private SelectJoinStep makeQueryWithProjections(
Selection selection, TableMetadata tableMetadata) {
if (selection.getProjections().isEmpty()) {
return DSL.using(SQLDialect.DEFAULT).select().from("Record r");
}
List projectedFields = new ArrayList<>();
// To project the required columns, we build a JSON object with the same structure as the
// `Record.class`so that each field can be deserialized properly into a `Record.class` object.
// For example, the projected field "r.id" will be mapped to the `Record.id` attribute
projectedFields.add("r.id");
projectedFields.add("r.concatenatedPartitionKey");
// Project partition key columns
addJsonFormattedProjectionsFieldForAttribute(
projectedFields,
"partitionKey",
selection.getProjections().stream().filter(tableMetadata.getPartitionKeyNames()::contains));
// Project clustering key columns
addJsonFormattedProjectionsFieldForAttribute(
projectedFields,
"clusteringKey",
selection.getProjections().stream()
.filter(tableMetadata.getClusteringKeyNames()::contains));
// Project non-primary key columns
addJsonFormattedProjectionsFieldForAttribute(
projectedFields,
"values",
selection.getProjections().stream()
.filter(
c ->
!tableMetadata.getPartitionKeyNames().contains(c)
&& !tableMetadata.getClusteringKeyNames().contains(c)));
return DSL.using(SQLDialect.DEFAULT)
.select(projectedFields.stream().map(DSL::field).collect(Collectors.toList()))
.from("Record r");
}
private void addJsonFormattedProjectionsFieldForAttribute(
List projectedFields, String rootAttributeName, Stream projectedColumnNames) {
// If rootAttributeName="partitionKey", the following will be mapped to the
// "Record.partitionKey" map upon the query result deserialization.
// For example, to project the partition keys c1 and c2, the partitionKey field will be
// `{"c1": r.partitionKey["c1"], "c2":r.partitionKey["c2"]} as partitionKey`
// Besides, since the Jooq parser consumes curly brace character as they are treated as
// placeholder, each curly brace need to be doubled "{{" to have a single curly brace "{"
// present in the generated sql query
List projectedColumnsToJson =
projectedColumnNames
.map(
columnName ->
"\""
+ columnName
+ "\":r."
+ rootAttributeName
+ CosmosUtils.quoteKeyword(columnName))
.collect(Collectors.toList());
if (!projectedColumnsToJson.isEmpty()) {
projectedFields.add(
"{{" + String.join(",", projectedColumnsToJson) + "}} as " + rootAttributeName);
}
}
private void setStart(SelectConditionStep select, Scan scan) {
scan.getStartClusteringKey()
.ifPresent(
k -> {
ValueBinder binder = new ValueBinder();
List> start = k.getColumns();
IntStream.range(0, start.size())
.forEach(
i -> {
Column> column = start.get(i);
Field