package org.rakam.postgresql.analysis; import com.facebook.presto.sql.RakamSqlFormatter; import com.google.common.collect.ImmutableList; import io.netty.handler.codec.http.HttpResponseStatus; import org.rakam.analysis.FunnelQueryExecutor; import org.rakam.collection.SchemaField; import org.rakam.config.ProjectConfig; import org.rakam.report.DelegateQueryExecution; import org.rakam.report.QueryExecution; 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.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import static com.facebook.presto.sql.RakamExpressionFormatter.formatIdentifier; import static java.lang.String.format; import static org.rakam.collection.FieldType.LONG; import static org.rakam.collection.FieldType.STRING; import static org.rakam.util.DateTimeUtils.TIMESTAMP_FORMATTER; import static org.rakam.util.ValidationUtil.checkCollection; import static org.rakam.util.ValidationUtil.checkTableColumn; public class FastGenericFunnelQueryExecutor implements FunnelQueryExecutor { private final ProjectConfig projectConfig; private final QueryExecutorService executor; @Inject public FastGenericFunnelQueryExecutor(QueryExecutorService executor, ProjectConfig projectConfig) { this.projectConfig = projectConfig; this.executor = executor; } @Override public QueryExecution query(String project, List<FunnelStep> steps, Optional<String> dimension, LocalDate startDate, LocalDate endDate, Optional<FunnelWindow> window, ZoneId zoneId, Optional<List<String>> connectors) { if (dimension.isPresent() && connectors.isPresent() && connectors.get().contains(dimension)) { throw new RakamException("Dimension and connector field cannot be equal", HttpResponseStatus.BAD_REQUEST); } List<String> selects = new ArrayList<>(); List<String> insideSelect = new ArrayList<>(); List<String> mainSelect = new ArrayList<>(); String connectorString = connectors.map(item -> item.stream().map(ValidationUtil::checkTableColumn).collect(Collectors.joining(", "))) .orElse(checkTableColumn(projectConfig.getUserColumn())); for (int i = 0; i < steps.size(); i++) { Optional<String> filterExp = steps.get(i).getExpression().map(value -> RakamSqlFormatter.formatExpression(value, name -> name.getParts().stream().map(e -> formatIdentifier(e, '"')).collect(Collectors.joining(".")), name -> name.getParts().stream() .map(e -> formatIdentifier(e, '"')).collect(Collectors.joining(".")), '"')); selects.add(format("sum(case when ts_event%d is not null then 1 else 0 end) as event%d_count", i, i)); insideSelect.add(format("min(case when step = %d then %s end) as ts_event%d", i, checkTableColumn(projectConfig.getTimeColumn()), i)); mainSelect.add(format("select %s %d as step, %s, %s from %s where %s between timestamp '%s' and timestamp '%s' and %s", dimension.map(v -> v + ", ").orElse(""), i, connectorString, checkTableColumn(projectConfig.getTimeColumn()), checkCollection(steps.get(i).getCollection()), checkTableColumn(projectConfig.getTimeColumn()), TIMESTAMP_FORMATTER.format(startDate.atStartOfDay(zoneId)), TIMESTAMP_FORMATTER.format(endDate.plusDays(1).atStartOfDay(zoneId)), filterExp.orElse("true"))); } String query = format("select %s %s\n" + "from (select %s,\n" + " %s %s" + " from (\n" + " %s" + " ) t \n" + " group by %s %s\n" + " ) t %s", dimension.map(v -> v + ", ").orElse(""), selects.stream().collect(Collectors.joining(",\n")), connectorString, dimension.map(v -> v + ", ").orElse(""), insideSelect.stream().collect(Collectors.joining(",\n")), mainSelect.stream().collect(Collectors.joining(" UNION ALL\n")), dimension.map(v -> v + ", ").orElse(""), connectorString, dimension.map(v -> " group by 1").orElse("")); QueryExecution queryExecution = executor.executeQuery(project, query); return new DelegateQueryExecution(queryExecution, result -> { if (result.isFailed()) { return result; } List<List<Object>> newResult = new ArrayList<>(); List<SchemaField> metadata; if (dimension.isPresent()) { metadata = ImmutableList.of( new SchemaField("step", STRING), new SchemaField("dimension", STRING), new SchemaField("count", LONG)); for (List<Object> objects : result.getResult()) { for (int i = 0; i < steps.size(); i++) { newResult.add(ImmutableList.of("Step " + i, objects.get(0), objects.get(i + 1))); } } } else { metadata = ImmutableList.of( new SchemaField("step", STRING), new SchemaField("count", LONG)); List<Object> stepCount = result.getResult().get(0); for (int i = 0; i < steps.size(); i++) { Object value = stepCount.get(i); newResult.add(ImmutableList.of("Step " + i, value == null ? 0 : value)); } } return new QueryResult(metadata, newResult, result.getProperties()); }); } }