package org.rakam.presto.plugin.user;
import com.facebook.presto.sql.tree.Expression;
import com.facebook.presto.sql.tree.QualifiedName;
import com.google.common.collect.ImmutableMap;
import org.rakam.analysis.ConfigManager;
import org.rakam.analysis.MaterializedViewService;
import org.rakam.analysis.metadata.Metastore;
import org.rakam.collection.SchemaField;
import org.rakam.config.ProjectConfig;
import org.rakam.plugin.MaterializedView;
import org.rakam.plugin.user.UserPluginConfig;
import org.rakam.postgresql.plugin.user.AbstractPostgresqlUserStorage;
import org.rakam.postgresql.report.PostgresqlQueryExecutor;
import org.rakam.presto.analysis.PrestoConfig;
import org.rakam.presto.analysis.PrestoQueryExecutor;
import org.rakam.report.QueryExecution;
import org.rakam.report.QueryExecutor;
import org.rakam.report.QueryExecutorService;
import org.rakam.report.QueryResult;
import org.rakam.util.RakamException;
import org.rakam.util.ValidationUtil;
import javax.inject.Inject;
import java.time.Duration;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static com.facebook.presto.sql.RakamSqlFormatter.formatExpression;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static java.lang.String.format;
import static org.rakam.presto.analysis.PrestoQueryExecution.PRESTO_TIMESTAMP_FORMAT;
import static org.rakam.presto.analysis.PrestoUserService.ANONYMOUS_ID_MAPPING;
import static org.rakam.report.realtime.AggregationType.COUNT;
import static org.rakam.util.ValidationUtil.checkCollection;
import static org.rakam.util.ValidationUtil.checkTableColumn;
public class PrestoExternalUserStorageAdapter
extends AbstractPostgresqlUserStorage
{
private final PrestoQueryExecutor executor;
private final UserPluginConfig config;
private final MaterializedViewService materializedViewService;
private final PrestoConfig prestoConfig;
private final Metastore metastore;
private final QueryExecutorService executorService;
private final ProjectConfig projectConfig;
@Inject
public PrestoExternalUserStorageAdapter(
ProjectConfig projectConfig,
MaterializedViewService materializedViewService,
PrestoQueryExecutor executor,
QueryExecutorService executorService,
PrestoConfig prestoConfig,
ConfigManager configManager,
UserPluginConfig config,
PostgresqlQueryExecutor queryExecutor,
Metastore metastore)
{
super(executorService, queryExecutor, configManager);
this.projectConfig = projectConfig;
this.executor = executor;
this.executorService = executorService;
this.config = config;
this.prestoConfig = prestoConfig;
this.materializedViewService = materializedViewService;
queryExecutor.executeRawStatement("CREATE SCHEMA IF NOT EXISTS users").getResult().thenAccept(result -> {
if (result.isFailed()) {
throw new IllegalStateException("Unable to create schema for users: " + result.getError().toString());
}
;
});
this.metastore = metastore;
}
@Override
public CompletableFuture<QueryResult> searchUsers(String project,
List<String> selectColumns,
Expression filterExpression,
List<EventFilter> eventFilter,
Sorting sortColumn, long limit,
String offset)
{
if (filterExpression != null && eventFilter != null && !eventFilter.isEmpty()) {
return super.searchUsers(project, selectColumns, filterExpression, eventFilter, sortColumn, limit, offset);
}
String query;
if (eventFilter != null && !eventFilter.isEmpty()) {
query = String.format("select distinct %s as %s from (%s) ",
checkTableColumn(projectConfig.getUserColumn()),
config.getIdentifierColumn(),
eventFilter.stream().map(f -> String.format(getEventFilterQuery(project, f), checkCollection(f.collection)))
.collect(Collectors.joining(" union all ")));
}
else {
List<Map.Entry<String, List<SchemaField>>> collections = metastore.getCollections(project)
.entrySet().stream()
.filter(c -> c.getValue().stream().anyMatch(a -> a.getName().equals("_user")))
.collect(Collectors.toList());
if (collections.isEmpty()) {
return QueryExecution.completedQueryExecution(null, QueryResult.empty()).getResult();
}
query = String.format("select distinct %s as %s from (%s)",
checkTableColumn(projectConfig.getUserColumn()),
config.getIdentifierColumn(),
collections.stream().map(c -> String.format("select %s from %s collection where collection.%s is not null", "_user",
checkCollection(c.getKey()), checkTableColumn(projectConfig.getUserColumn()))).collect(Collectors.joining(" union all ")));
}
// if(sortColumn == null) {
// sortColumn = new Sorting("_user", Ordering.asc);
// }
return executorService.executeQuery(project, query +
(sortColumn == null ? "" : (" ORDER BY " + sortColumn.column + " " + sortColumn.order.name()))
+ " LIMIT " + limit).getResult();
}
@Override
public void createSegment(String project, String name, String tableName, Expression filterExpression, List<EventFilter> eventFilter, Duration interval)
{
String query;
if (filterExpression == null) {
query = String.format("select distinct %s as id from (%s) t",
checkTableColumn(projectConfig.getUserColumn()),
eventFilter.stream().map(f -> String.format(getEventFilterQuery(project, f), f.collection)).collect(Collectors.joining(" UNION ALL ")));
}
else {
throw new RakamException("User segment must have at least one event filter", BAD_REQUEST);
}
materializedViewService.create(project, new MaterializedView(tableName,
"Users who did " + (tableName == null ? "at least one event" : tableName + " event"),
query, interval, null, null, ImmutableMap.of()));
}
@Override
public QueryExecutorService getExecutorForWithEventFilter()
{
return executorService;
}
@Override
public List<String> getEventFilterPredicate(String project, List<EventFilter> eventFilter)
{
return eventFilter.stream().map(f -> String.format("id in (%s)",
String.format(getEventFilterQuery(project, f), executor.formatTableReference(project, QualifiedName.of(f.collection), Optional.empty(), ImmutableMap.of(), "collection"))))
.collect(Collectors.toList());
}
public String getEventFilterQuery(String project, EventFilter filter)
{
StringBuilder builder = new StringBuilder();
builder.append("select ")
.append(config.getEnableUserMapping() ? "coalesce(mapping._user, collection._user, collection._device_id) as _user" : "collection._user")
.append(" from %s collection");
ArrayList<String> filterList = new ArrayList<>(3);
if (filter.filterExpression != null) {
filterList.add(formatExpression(filter.getExpression(),
reference -> reference.getParts().stream()
.map(ValidationUtil::checkLiteral).collect(Collectors.joining(".")), '"'));
}
if (filter.timeframe != null) {
if (filter.timeframe.start != null) {
filterList.add(String.format("collection.%s > cast('%s' as timestamp)",
checkTableColumn(projectConfig.getTimeColumn()),
PRESTO_TIMESTAMP_FORMAT.format(filter.timeframe.start.atZone(ZoneId.of("UTC")))));
}
if (filter.timeframe.end != null) {
filterList.add(String.format("collection.%s < cast('%s' as timestamp)",
checkTableColumn(projectConfig.getTimeColumn()),
PRESTO_TIMESTAMP_FORMAT.format(filter.timeframe.end.atZone(ZoneId.of("UTC")))));
}
}
if (config.getEnableUserMapping() && getHasDeviceId(project, filter.collection)) {
// mapping.created_at <= max and mapping.merged_at > min
builder.append(String.format(" left join %s mapping on (collection.%s is null and mapping.id = collection._device_id)",
checkCollection(ANONYMOUS_ID_MAPPING), checkTableColumn(projectConfig.getUserColumn())));
}
builder.append(" where ").append(" collection." + checkTableColumn(projectConfig.getUserColumn()) + " is not null ");
if (!filterList.isEmpty()) {
builder.append("and ")
.append(filterList.stream()
.collect(Collectors.joining(" AND ")));
}
if (filter.aggregation != null) {
String field;
if (filter.aggregation.type == COUNT && filter.aggregation.field == null) {
field = "collection._user";
}
else {
field = "collection.\"" + checkTableColumn(filter.aggregation.field, "aggregation field", '"') + "\"";
}
if (config.getEnableUserMapping()) {
builder.append(" group by mapping._user, collection._user, collection._device_id");
}
else {
builder.append(" group by collection._user");
}
if (filter.aggregation.minimum != null || filter.aggregation.maximum != null) {
builder.append(" having ");
}
if (filter.aggregation.minimum != null) {
builder.append(format(" %s(%s) >= %d ", filter.aggregation.type, field, filter.aggregation.minimum));
}
if (filter.aggregation.maximum != null) {
if (filter.aggregation.minimum != null) {
builder.append(" and ");
}
builder.append(format(" %s(%s) < %d ", filter.aggregation.type, field, filter.aggregation.maximum));
}
}
return builder.toString();
}
private boolean getHasDeviceId(String project, String collection)
{
return metastore.getCollection(project, collection).stream()
.anyMatch(f -> f.getName().equals("_device_id"));
}
@Override
public String getUserTable(String project, boolean isEventFilterActive)
{
if (isEventFilterActive) {
return prestoConfig.getUserConnector() + ".users." + project;
}
return "users." + project;
}
}