/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rakam.analysis;
import com.google.common.collect.ImmutableList;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.analysis.metadata.Metastore;
import org.rakam.collection.SchemaField;
import org.rakam.config.ProjectConfig;
import org.rakam.report.DelegateQueryExecution;
import org.rakam.report.QueryExecution;
import org.rakam.report.QueryExecutor;
import org.rakam.report.QueryResult;
import org.rakam.util.RakamException;
import org.rakam.util.ValidationUtil;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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.checkTableColumn;
public abstract class AbstractFunnelQueryExecutor
implements FunnelQueryExecutor
{
private static final String CONNECTOR_FIELD = "_user";
private final QueryExecutor executor;
private final Metastore metastore;
protected final ProjectConfig projectConfig;
public AbstractFunnelQueryExecutor(ProjectConfig projectConfig, Metastore metastore, QueryExecutor executor)
{
this.projectConfig = projectConfig;
this.metastore = metastore;
this.executor = executor;
}
public abstract String getTemplate(List<FunnelStep> steps, Optional<String> dimension, Optional<FunnelWindow> window);
public abstract String convertFunnel(String project, String connectorField, int idx, FunnelStep funnelStep, Optional<String> dimension, LocalDate startDate, LocalDate endDate);
@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(connectors.isPresent()) {
throw new RakamException("Custom connectors are not supported", HttpResponseStatus.BAD_REQUEST);
}
Map<String, List<SchemaField>> collections = metastore.getCollections(project);
String ctes = IntStream.range(0, steps.size())
.mapToObj(i -> convertFunnel(
project, testDeviceIdExists(steps.get(i), collections) ? "coalesce(cast(%s." + checkTableColumn(projectConfig.getUserColumn()) + " as varchar), _device_id)" : "_user", i,
steps.get(i), dimension, startDate, endDate))
.collect(Collectors.joining(" UNION ALL "));
String dimensionCol = dimension.map(ValidationUtil::checkTableColumn).map(v -> v + ", ").orElse("");
String query = format(getTemplate(steps, dimension, window), dimensionCol, dimensionCol, ctes,
TIMESTAMP_FORMATTER.format(startDate.atStartOfDay(zoneId)),
TIMESTAMP_FORMATTER.format(endDate.plusDays(1).atStartOfDay(zoneId)),
dimensionCol, CONNECTOR_FIELD,
dimension.map(v -> ", 2").orElse(""));
if (dimension.isPresent()) {
query = String.format("SELECT (CASE WHEN rank > 15 THEN 'Others' ELSE cast(%s as varchar) END) as dimension, step, sum(total) from " +
"(select *, row_number() OVER(ORDER BY total DESC) rank from (%s) t) t GROUP BY 1, 2",
dimension.map(ValidationUtil::checkTableColumn).get(), query);
}
QueryExecution queryExecution = executor.executeRawQuery(query);
return new DelegateQueryExecution(queryExecution,
result -> {
if (result.isFailed()) {
return result;
}
List<List<Object>> newResult;
List<List<Object>> queryResult = result.getResult();
List<SchemaField> metadata;
if (dimension.isPresent()) {
newResult = new ArrayList<>();
Map<Object, List<List<Object>>> collect = queryResult.stream().collect(Collectors.groupingBy(x -> x.get(0) == null ? "null" : x.get(0)));
for (Map.Entry<Object, List<List<Object>>> entry : collect.entrySet()) {
List<List<Object>> subResult = IntStream.range(0, steps.size())
.mapToObj(i -> Arrays.asList("Step " + (i + 1), entry.getKey(), 0L))
.collect(Collectors.toList());
for (int step = 0; step < subResult.size(); step++) {
int finalStep = step;
entry.getValue().stream().filter(e -> ((Number) e.get(1)).longValue() >= finalStep + 1).map(e -> e.get(2))
.forEach(val -> subResult.get(finalStep).set(2,
((Number) subResult.get(finalStep).get(2)).longValue() + ((Number) val).longValue()));
}
newResult.addAll(subResult);
}
metadata = ImmutableList.of(
new SchemaField("step", STRING),
new SchemaField("dimension", STRING),
new SchemaField("count", LONG));
}
else {
newResult = IntStream.range(0, steps.size())
.mapToObj(i -> Arrays.<Object>asList("Step " + (i + 1), 0L))
.collect(Collectors.toList());
for (int step = 0; step < newResult.size(); step++) {
int finalStep = step;
queryResult.stream().filter(e -> ((Number) e.get(0)).intValue() >= finalStep + 1).map(e -> e.get(1))
.forEach(val -> newResult.get(finalStep).set(1,
((Number) newResult.get(finalStep).get(1)).longValue() + ((Number) val).longValue()));
}
metadata = ImmutableList.of(
new SchemaField("step", STRING),
new SchemaField("count", LONG));
}
return new QueryResult(metadata, newResult, result.getProperties());
});
}
protected boolean testDeviceIdExists(FunnelStep firstAction, Map<String, List<SchemaField>> collections)
{
List<SchemaField> schemaFields = collections.get(firstAction.getCollection());
if (schemaFields == null) {
throw new RakamException("The collection in first action does not exist.", HttpResponseStatus.BAD_REQUEST);
}
return schemaFields.stream().anyMatch(e -> e.getName().equals("_device_id"));
}
}