package org.rakam.presto;
import com.facebook.presto.sql.tree.QualifiedName;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Binder;
import com.google.inject.BindingAnnotation;
import com.google.inject.Key;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.multibindings.OptionalBinder;
import com.google.inject.name.Names;
import org.rakam.analysis.ApiKeyService;
import org.rakam.analysis.ConfigManager;
import org.rakam.analysis.ContinuousQueryService;
import org.rakam.analysis.EscapeIdentifier;
import org.rakam.analysis.EventExplorer;
import org.rakam.analysis.FunnelQueryExecutor;
import org.rakam.analysis.JDBCPoolDataSource;
import org.rakam.analysis.MaterializedViewService;
import org.rakam.analysis.RealtimeService;
import org.rakam.analysis.RealtimeService.RealtimeAggregations;
import org.rakam.analysis.RetentionQueryExecutor;
import org.rakam.analysis.TimestampToEpochFunction;
import org.rakam.analysis.metadata.JDBCQueryMetadata;
import org.rakam.analysis.metadata.Metastore;
import org.rakam.analysis.metadata.QueryMetadataStore;
import org.rakam.aws.kinesis.ForStreamer;
import org.rakam.config.JDBCConfig;
import org.rakam.config.MetadataConfig;
import org.rakam.config.ProjectConfig;
import org.rakam.plugin.CopyEvent;
import org.rakam.plugin.EventMapper;
import org.rakam.plugin.RakamModule;
import org.rakam.plugin.SystemEvents.ProjectCreatedEvent;
import org.rakam.plugin.TimestampEventMapper;
import org.rakam.plugin.stream.EventStream;
import org.rakam.plugin.stream.EventStreamConfig;
import org.rakam.plugin.user.AbstractUserService;
import org.rakam.plugin.user.UserPluginConfig;
import org.rakam.postgresql.PostgresqlConfigManager;
import org.rakam.postgresql.analysis.FastGenericFunnelQueryExecutor;
import org.rakam.postgresql.analysis.JDBCApiKeyService;
import org.rakam.postgresql.plugin.user.AbstractPostgresqlUserStorage;
import org.rakam.presto.analysis.MysqlConfigManager;
import org.rakam.presto.analysis.PrestoConfig;
import org.rakam.presto.analysis.PrestoContinuousQueryService;
import org.rakam.presto.analysis.PrestoEventExplorer;
import org.rakam.presto.analysis.PrestoEventStream;
import org.rakam.presto.analysis.PrestoMaterializedViewService;
import org.rakam.presto.analysis.PrestoMetastore;
import org.rakam.presto.analysis.PrestoQueryExecutor;
import org.rakam.presto.analysis.PrestoRakamRaptorMetastore;
import org.rakam.presto.analysis.PrestoRetentionQueryExecutor;
import org.rakam.presto.analysis.PrestoUserService;
import org.rakam.presto.collection.PrestoCopyEvent;
import org.rakam.presto.plugin.user.PrestoExternalUserStorageAdapter;
import org.rakam.report.QueryExecutor;
import org.rakam.report.eventexplorer.EventExplorerConfig;
import org.rakam.report.realtime.AggregationType;
import org.rakam.report.realtime.RealTimeConfig;
import org.rakam.util.ConditionalModule;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Optional;
import static io.airlift.configuration.ConfigBinder.configBinder;
import static io.airlift.http.client.HttpClientBinder.httpClientBinder;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static java.lang.String.format;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.rakam.presto.analysis.PrestoUserService.ANONYMOUS_ID_MAPPING;
import static org.rakam.report.realtime.AggregationType.APPROXIMATE_UNIQUE;
import static org.rakam.report.realtime.AggregationType.COUNT;
import static org.rakam.report.realtime.AggregationType.MAXIMUM;
import static org.rakam.report.realtime.AggregationType.MINIMUM;
import static org.rakam.report.realtime.AggregationType.SUM;
import static org.rakam.util.ValidationUtil.checkCollection;
@AutoService(RakamModule.class)
@ConditionalModule(config = "store.adapter", value = "presto")
public class PrestoModule
extends RakamModule
{
@Override
protected void setup(Binder binder)
{
configBinder(binder).bindConfig(MetadataConfig.class);
configBinder(binder).bindConfig(PrestoConfig.class);
PrestoConfig prestoConfig = buildConfigObject(PrestoConfig.class);
OptionalBinder<JDBCConfig> userConfig = OptionalBinder.newOptionalBinder(binder, Key.get(JDBCConfig.class, UserConfig.class));
binder.bind(QueryExecutor.class).to(PrestoQueryExecutor.class);
binder.bind(char.class).annotatedWith(EscapeIdentifier.class).toInstance('"');
binder.bind(MaterializedViewService.class).to(PrestoMaterializedViewService.class);
binder.bind(String.class).annotatedWith(TimestampToEpochFunction.class).toInstance("to_unixtime");
OptionalBinder.newOptionalBinder(binder, CopyEvent.class)
.setBinding().to(PrestoCopyEvent.class);
buildConfigObject(JDBCConfig.class, "report.metadata.store.jdbc");
JDBCPoolDataSource metadataDataSource;
if ("rakam_raptor".equals(prestoConfig.getColdStorageConnector())) {
if (prestoConfig.getEnableStreaming()) {
binder.bind(ContinuousQueryService.class).to(PrestoContinuousQueryService.class);
}
else {
binder.bind(ContinuousQueryService.class).to(PrestoPseudoContinuousQueryService.class);
}
metadataDataSource = bindJDBCConfig(binder, "presto.metastore.jdbc");
if (buildConfigObject(EventStreamConfig.class).getEventStreamEnabled()) {
httpClientBinder(binder).bindHttpClient("streamer", ForStreamer.class);
binder.bind(EventStream.class).to(PrestoEventStream.class).in(Scopes.SINGLETON);
}
}
else {
metadataDataSource = bindJDBCConfig(binder, "report.metadata.store.jdbc");
binder.bind(ContinuousQueryService.class).to(PrestoPseudoContinuousQueryService.class);
}
binder.bind(ApiKeyService.class).toInstance(new JDBCApiKeyService(metadataDataSource));
binder.bind(new TypeLiteral<List<AggregationType>>() {}).annotatedWith(RealtimeAggregations.class)
.toInstance(ImmutableList.of(
COUNT, SUM, MINIMUM, MAXIMUM, APPROXIMATE_UNIQUE));
binder.bind(RealtimeService.class).to(PrestoRealtimeService.class);
// use same jdbc pool if report.metadata.store is not set explicitly.
if (getConfig("report.metadata.store") == null) {
binder.bind(JDBCPoolDataSource.class)
.annotatedWith(Names.named("report.metadata.store.jdbc"))
.toInstance(metadataDataSource);
String url = metadataDataSource.getConfig().getUrl();
if (url.startsWith("jdbc:mysql")) {
binder.bind(ConfigManager.class).to(MysqlConfigManager.class);
}
else if (url.startsWith("jdbc:postgresql")) {
binder.bind(ConfigManager.class).to(PostgresqlConfigManager.class);
}
else {
throw new IllegalStateException(format("Invalid report metadata database: %s", url));
}
binder.bind(QueryMetadataStore.class).to(JDBCQueryMetadata.class)
.in(Scopes.SINGLETON);
}
if ("rakam_raptor".equals(prestoConfig.getColdStorageConnector())) {
binder.bind(Metastore.class).to(PrestoRakamRaptorMetastore.class).in(Scopes.SINGLETON);
}
else {
binder.bind(Metastore.class).to(PrestoMetastore.class).in(Scopes.SINGLETON);
}
if ("postgresql".equals(getConfig("plugin.user.storage"))) {
binder.bind(AbstractPostgresqlUserStorage.class).to(PrestoExternalUserStorageAdapter.class)
.in(Scopes.SINGLETON);
binder.bind(AbstractUserService.class).to(PrestoUserService.class)
.in(Scopes.SINGLETON);
userConfig.setBinding().toInstance(buildConfigObject(JDBCConfig.class, "store.adapter.postgresql"));
}
EventExplorerConfig eventExplorerConfig = buildConfigObject(EventExplorerConfig.class);
if (eventExplorerConfig.isEventExplorerEnabled()) {
binder.bind(EventExplorer.class).to(PrestoEventExplorer.class);
}
UserPluginConfig userPluginConfig = buildConfigObject(UserPluginConfig.class);
if (userPluginConfig.getEnableUserMapping()) {
binder.bind(UserMergeTableHook.class).asEagerSingleton();
}
if (userPluginConfig.isFunnelAnalysisEnabled()) {
binder.bind(FunnelQueryExecutor.class).to(FastGenericFunnelQueryExecutor.class);
}
if (userPluginConfig.isRetentionAnalysisEnabled()) {
binder.bind(RetentionQueryExecutor.class).to(PrestoRetentionQueryExecutor.class);
}
Multibinder<EventMapper> timeMapper = Multibinder.newSetBinder(binder, EventMapper.class);
timeMapper.addBinding().to(TimestampEventMapper.class).in(Scopes.SINGLETON);
}
@Override
public String name()
{
return "PrestoDB backend for Rakam";
}
@Override
public String description()
{
return "Rakam backend for high-throughput systems.";
}
private JDBCPoolDataSource bindJDBCConfig(Binder binder, String config)
{
JDBCPoolDataSource dataSource = JDBCPoolDataSource.getOrCreateDataSource(
buildConfigObject(JDBCConfig.class, config));
binder.bind(JDBCPoolDataSource.class)
.annotatedWith(Names.named(config))
.toInstance(dataSource);
return dataSource;
}
public static class UserMergeTableHook
{
private final PrestoQueryExecutor executor;
private final ProjectConfig projectConfig;
@Inject
public UserMergeTableHook(ProjectConfig projectConfig, PrestoQueryExecutor executor)
{
this.projectConfig = projectConfig;
this.executor = executor;
}
@Subscribe
public void onCreateProject(ProjectCreatedEvent event)
{
executor.executeRawStatement(format("CREATE TABLE %s(id VARCHAR, %s VARCHAR, " +
"created_at TIMESTAMP, merged_at TIMESTAMP)",
executor.formatTableReference(event.project, QualifiedName.of(ANONYMOUS_ID_MAPPING), Optional.empty(), ImmutableMap.of(), "collection"),
checkCollection(projectConfig.getUserColumn())));
}
}
public static class PrestoRealtimeService
extends RealtimeService
{
@Inject
public PrestoRealtimeService(ProjectConfig projectConfig, ContinuousQueryService service, QueryExecutor executor, @RealtimeAggregations List<AggregationType> aggregationTypes, RealTimeConfig config, @TimestampToEpochFunction String timestampToEpochFunction, @EscapeIdentifier char escapeIdentifier)
{
super(projectConfig, service, executor, aggregationTypes, config, timestampToEpochFunction, escapeIdentifier);
}
@Override
public String getIntermediateFunction(AggregationType type)
{
String format;
switch (type) {
case MAXIMUM:
format = "max(%s)";
break;
case MINIMUM:
format = "min(%s)";
break;
case COUNT:
format = "count(%s)";
break;
case SUM:
format = "sum(%s)";
break;
case APPROXIMATE_UNIQUE:
format = "approx_set(%s)";
break;
case COUNT_UNIQUE:
format = "set(%s)";
break;
default:
throw new RakamException("Aggregation type couldn't found.", BAD_REQUEST);
}
return format;
}
@Override
public String combineFunction(AggregationType aggregationType)
{
switch (aggregationType) {
case COUNT:
case SUM:
return "sum(%s)";
case MINIMUM:
return "min(%s)";
case MAXIMUM:
return "max(%s)";
case APPROXIMATE_UNIQUE:
return "cardinality(merge(%s))";
case COUNT_UNIQUE:
return "cardinality(merge(%s))";
default:
throw new RakamException("Aggregation type couldn't found.", BAD_REQUEST);
}
}
}
@BindingAnnotation
@Target({FIELD, PARAMETER, METHOD})
@Retention(RUNTIME)
public @interface UserConfig {}
}