package org.rakam.aws.dynamodb.metastore;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
import com.amazonaws.services.dynamodbv2.model.Condition;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTableResult;
import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.QueryResult;
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.analysis.metadata.QueryMetadataStore;
import org.rakam.aws.AWSConfig;
import org.rakam.plugin.ContinuousQuery;
import org.rakam.plugin.MaterializedView;
import org.rakam.util.JsonHelper;
import org.rakam.util.NotExistsException;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static com.amazonaws.services.dynamodbv2.model.ComparisonOperator.BEGINS_WITH;
import static com.google.common.collect.ImmutableMap.of;
public class DynamodbQueryMetastore
implements QueryMetadataStore
{
private static final List<KeySchemaElement> PROJECT_KEYSCHEMA = ImmutableList.of(
new KeySchemaElement().withKeyType(KeyType.HASH).withAttributeName("project"),
new KeySchemaElement().withKeyType(KeyType.RANGE).withAttributeName("type_table_name")
);
private static final Set<AttributeDefinition> ATTRIBUTES = ImmutableSet.of(
new AttributeDefinition().withAttributeName("project").withAttributeType(ScalarAttributeType.S),
new AttributeDefinition().withAttributeName("type_table_name").withAttributeType(ScalarAttributeType.S)
);
private final AmazonDynamoDBClient dynamoDBClient;
private final DynamodbQueryMetastoreConfig tableConfig;
@Inject
public DynamodbQueryMetastore(AWSConfig config, DynamodbQueryMetastoreConfig tableConfig)
{
dynamoDBClient = new AmazonDynamoDBClient(config.getCredentials());
dynamoDBClient.setRegion(config.getAWSRegion());
if (config.getDynamodbEndpoint() != null) {
dynamoDBClient.setEndpoint(config.getDynamodbEndpoint());
}
this.tableConfig = tableConfig;
}
@PostConstruct
public void setup()
{
try {
DescribeTableResult table = dynamoDBClient.describeTable(tableConfig.getTableName());
if (!table.getTable().getKeySchema().equals(PROJECT_KEYSCHEMA)) {
throw new IllegalStateException("Dynamodb table for query metadata store has invalid key schema");
}
if (!ImmutableSet.copyOf(table.getTable().getAttributeDefinitions()).equals(ATTRIBUTES)) {
throw new IllegalStateException("Dynamodb table for query metadata store has invalid attribute schema");
}
}
catch (ResourceNotFoundException e) {
createTable();
}
}
private void createTable()
{
dynamoDBClient.createTable(new CreateTableRequest()
.withTableName(tableConfig.getTableName()).withKeySchema(PROJECT_KEYSCHEMA)
.withAttributeDefinitions(ATTRIBUTES)
.withProvisionedThroughput(new ProvisionedThroughput()
.withReadCapacityUnits(1L)
.withWriteCapacityUnits(1L)));
}
@Override
public void createMaterializedView(String project, MaterializedView materializedView)
{
dynamoDBClient.putItem(new PutItemRequest().withTableName(tableConfig.getTableName())
.withItem(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("materialized_" + materializedView.tableName),
"value", new AttributeValue(JsonHelper.encode(materializedView)))));
}
@Override
public void deleteMaterializedView(String project, String tableName)
{
dynamoDBClient.deleteItem(new DeleteItemRequest().withTableName(tableConfig.getTableName())
.withKey(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("materialized_" + tableName))));
}
@Override
public MaterializedView getMaterializedView(String project, String tableName)
{
Map<String, AttributeValue> item = dynamoDBClient.getItem(new GetItemRequest().withTableName(tableConfig.getTableName())
.withAttributesToGet("value")
.withKey(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("materialized_" + tableName)))).getItem();
if (item == null) {
throw new NotExistsException("Materialized view");
}
return JsonHelper.read(item.get("value").getS(), MaterializedView.class);
}
@Override
public List<MaterializedView> getMaterializedViews(String project)
{
List<Map<String, AttributeValue>> items = dynamoDBClient.scan(new ScanRequest()
.withTableName(tableConfig.getTableName())
.withFilterExpression("#P = :pValue AND begins_with(type_table_name, :prefix)")
.withExpressionAttributeNames(of("#P", "project"))
.withExpressionAttributeValues(of(
":pValue", new AttributeValue(project),
":prefix", new AttributeValue("materialized_")))).getItems();
return items.stream()
.map(item -> JsonHelper.read(item.get("value").getS(), MaterializedView.class))
.collect(Collectors.toList());
}
@Override
public boolean updateMaterializedView(String project, MaterializedView view, CompletableFuture<Instant> releaseLock)
{
throw new UnsupportedOperationException();
}
@Override
public void createContinuousQuery(String project, ContinuousQuery report)
{
dynamoDBClient.putItem(new PutItemRequest().withTableName(tableConfig.getTableName())
.withItem(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("continuous_" + report.tableName),
"value", new AttributeValue(JsonHelper.encode(report)))));
}
@Override
public void deleteContinuousQuery(String project, String tableName)
{
dynamoDBClient.deleteItem(new DeleteItemRequest().withTableName(tableConfig.getTableName())
.withKey(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("continuous_" + tableName))));
}
@Override
public List<ContinuousQuery> getContinuousQueries(String project)
{
List<Map<String, AttributeValue>> items = dynamoDBClient.scan(new ScanRequest()
.withTableName(tableConfig.getTableName())
.withFilterExpression("#P = :pValue AND begins_with(type_table_name, :prefix)")
.withExpressionAttributeNames(of("#P", "project"))
.withExpressionAttributeValues(of(
":pValue", new AttributeValue(project),
":prefix", new AttributeValue("continuous_")))).getItems();
return items.stream()
.map(item -> JsonHelper.read(item.get("value").getS(), ContinuousQuery.class))
.collect(Collectors.toList());
}
@Override
public ContinuousQuery getContinuousQuery(String project, String tableName)
{
Map<String, AttributeValue> item = dynamoDBClient.getItem(new GetItemRequest().withTableName(tableConfig.getTableName())
.withAttributesToGet("value")
.withKey(of(
"project", new AttributeValue(project),
"type_table_name", new AttributeValue("continuous_" + tableName)))).getItem();
if (item == null) {
throw new NotExistsException("Materialized view");
}
return JsonHelper.read(item.get("value").getS(), ContinuousQuery.class);
}
}