package org.rakam.report; import com.facebook.presto.sql.RakamSqlFormatter; import com.facebook.presto.sql.parser.ParsingException; import com.facebook.presto.sql.parser.SqlParser; import com.facebook.presto.sql.tree.Call; import com.facebook.presto.sql.tree.QualifiedName; import com.facebook.presto.sql.tree.Query; import com.facebook.presto.sql.tree.QuerySpecification; import com.facebook.presto.sql.tree.Statement; import com.google.inject.Inject; import io.netty.handler.codec.http.HttpResponseStatus; import org.rakam.analysis.EscapeIdentifier; import org.rakam.analysis.MaterializedViewService; import org.rakam.analysis.MaterializedViewService.MaterializedViewExecution; import org.rakam.analysis.metadata.Metastore; import org.rakam.collection.SchemaField; import org.rakam.plugin.MaterializedView; import org.rakam.util.LogUtil; import org.rakam.util.MaterializedViewNotExists; import org.rakam.util.NotExistsException; import org.rakam.util.RakamException; import java.time.Clock; import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static java.lang.String.format; import static org.rakam.report.QueryResult.EXECUTION_TIME; public class QueryExecutorService { private final SqlParser parser = new SqlParser(); public static final int DEFAULT_QUERY_RESULT_COUNT = 50000; public static final int MAX_QUERY_RESULT_LIMIT = 1000000; private final QueryExecutor executor; private final MaterializedViewService materializedViewService; private final Metastore metastore; private final char escapeIdentifier; private volatile Set<String> projectCache; @Inject public QueryExecutorService(QueryExecutor executor, Metastore metastore, MaterializedViewService materializedViewService, Clock clock, @EscapeIdentifier char escapeIdentifier) { this.executor = executor; this.materializedViewService = materializedViewService; this.metastore = metastore; this.escapeIdentifier = escapeIdentifier; } public QueryExecution executeQuery(String project, String sqlQuery, Optional<QuerySampling> sample, String defaultSchema, int limit) { if (!projectExists(project)) { throw new NotExistsException("Project"); } HashMap<MaterializedView, MaterializedViewExecution> materializedViews = new HashMap<>(); Map<String, String> sessionParameters = new HashMap<>(); String query; try { query = buildQuery(project, sqlQuery, sample, defaultSchema, limit, materializedViews, sessionParameters); } catch (ParsingException e) { QueryError error = new QueryError(e.getMessage(), null, null, e.getLineNumber(), e.getColumnNumber()); LogUtil.logQueryError(sqlQuery, error, executor.getClass()); return QueryExecution.completedQueryExecution(sqlQuery, QueryResult.errorResult(error, sqlQuery)); } long startTime = System.currentTimeMillis(); List<MaterializedViewExecution> queryExecutions = materializedViews.values().stream() .filter(m -> m.queryExecution != null) .collect(Collectors.toList()); if (queryExecutions.isEmpty()) { QueryExecution execution = executor.executeRawQuery(query, sessionParameters); if (materializedViews.isEmpty()) { return execution; } else { Map<String, Long> collect = materializedViews.entrySet().stream().collect(Collectors.toMap(v -> v.getKey().tableName, v -> v.getKey().lastUpdate != null ? v.getKey().lastUpdate.toEpochMilli() : -1)); return new DelegateQueryExecution(execution, result -> { result.setProperty("materializedViews", collect); return result; }); } } else { List<QueryExecution> executions = queryExecutions.stream() .filter(e -> e.queryExecution != null) .map(e -> e.queryExecution) .collect(Collectors.toList()); return new DelegateQueryExecution(new ChainQueryExecution(executions, query, (results) -> { for (MaterializedViewExecution queryExecution : queryExecutions) { QueryResult result = queryExecution.queryExecution.getResult().join(); if (result.isFailed()) { return new DelegateQueryExecution(queryExecution.queryExecution, materializedQueryUpdateResult -> { QueryError error = materializedQueryUpdateResult.getError(); String message = format("Error while updating materialized table '%s': %s", queryExecution.computeQuery, error.message); QueryError error1 = new QueryError(message, error.sqlState, error.errorCode, error.errorLine, error.charPositionInLine); LogUtil.logQueryError(query, error1, executor.getClass()); return QueryResult.errorResult(error1, query); }); } } return executor.executeRawQuery(query, sessionParameters); }), result -> { if (!result.isFailed()) { Map<String, Long> collect = materializedViews.entrySet().stream() .collect(Collectors.toMap( v -> v.getKey().tableName, v -> Optional.ofNullable(v.getKey().lastUpdate).map(Instant::toEpochMilli).orElse(0L))); result.setProperty("materializedViews", collect); result.setProperty(EXECUTION_TIME, System.currentTimeMillis() - startTime); } return result; }); } } public QueryExecution executeQuery(String project, String sqlQuery) { return executeQuery(project, sqlQuery, Optional.empty(), "collection", DEFAULT_QUERY_RESULT_COUNT); } public QueryExecution executeStatement(String project, String sqlQuery) { return executeQuery(project, sqlQuery); } private synchronized void updateProjectCache() { projectCache = metastore.getProjects(); } private boolean projectExists(String project) { if (projectCache == null) { updateProjectCache(); } if (!projectCache.contains(project)) { updateProjectCache(); if (!projectCache.contains(project)) { return false; } } return true; } public String buildQuery(String project, String query, Optional<QuerySampling> sample, String defaultSchema, Integer maxLimit, Map<MaterializedView, MaterializedViewExecution> materializedViews, Map<String, String> sessionParameters) { Query statement; Function<QualifiedName, String> tableNameMapper = tableNameMapper(project, materializedViews, sample, defaultSchema, sessionParameters); synchronized (parser) { Statement queryStatement = parser.createStatement(query); if ((queryStatement instanceof Query)) { statement = (Query) queryStatement; } else if ((queryStatement instanceof Call)) { StringBuilder builder = new StringBuilder(); new RakamSqlFormatter.Formatter(builder, tableNameMapper, escapeIdentifier) .process(queryStatement, 1); return builder.toString(); } else { throw new RakamException(queryStatement.getClass().getSimpleName() + " is not supported", BAD_REQUEST); } } StringBuilder builder = new StringBuilder(); new RakamSqlFormatter.Formatter(builder, tableNameMapper, escapeIdentifier) .process(statement, 1); if (maxLimit != null) { Integer limit = null; if (statement.getLimit().isPresent()) { limit = Integer.parseInt(statement.getLimit().get()); } if (statement.getQueryBody() instanceof QuerySpecification && ((QuerySpecification) statement.getQueryBody()).getLimit().isPresent()) { limit = Integer.parseInt(((QuerySpecification) statement.getQueryBody()).getLimit().get()); } if (limit != null) { if (limit > maxLimit) { throw new RakamException(format("The maximum value of LIMIT statement is %s", maxLimit), BAD_REQUEST); } } else { builder.append(" LIMIT ").append(maxLimit); } } return builder.toString(); } private Function<QualifiedName, String> tableNameMapper(String project, Map<MaterializedView, MaterializedViewExecution> materializedViews, Optional<QuerySampling> sample, String defaultSchema, Map<String, String> sessionParameters) { return (node) -> { if (node.getPrefix().isPresent() && node.getPrefix().get().toString().equals("materialized")) { MaterializedView materializedView; try { materializedView = materializedViewService.get(project, node.getSuffix()); } catch (NotExistsException e) { throw new MaterializedViewNotExists(node.getSuffix()); } MaterializedViewExecution materializedViewExecution = materializedViews.computeIfAbsent(materializedView, (key) -> materializedViewService.lockAndUpdateView(project, materializedView)); if (materializedViewExecution == null) { throw new IllegalStateException(); } return materializedViewExecution.computeQuery ; } return executor.formatTableReference(project, node, sample, sessionParameters, defaultSchema); }; } public CompletableFuture<List<SchemaField>> metadata(String project, String query) { StringBuilder builder = new StringBuilder(); Query queryStatement; try { queryStatement = (Query) parser.createStatement(checkNotNull(query, "query is required")); } catch (Exception e) { throw new RakamException("Unable to parse query: " + e.getMessage(), BAD_REQUEST); } Map<String, String> map = new HashMap<>(); new RakamSqlFormatter.Formatter(builder, qualifiedName -> executor.formatTableReference(project, qualifiedName, Optional.empty(), map, "collection"), escapeIdentifier) .process(queryStatement, 1); QueryExecution execution = executor .executeRawQuery(builder.toString() + " limit 0", map); CompletableFuture<List<SchemaField>> f = new CompletableFuture<>(); execution.getResult().thenAccept(result -> { if (result.isFailed()) { f.completeExceptionally(new RakamException(result.getError().message, HttpResponseStatus.INTERNAL_SERVER_ERROR)); } else { f.complete(result.getMetadata()); } }); return f; } }