package org.rakam.postgresql.analysis; import com.facebook.presto.sql.RakamSqlFormatter; import com.facebook.presto.sql.parser.SqlParser; import com.facebook.presto.sql.tree.QualifiedName; import com.facebook.presto.sql.tree.Query; import com.google.common.collect.ImmutableMap; import org.rakam.analysis.MaterializedViewService; import org.rakam.analysis.metadata.QueryMetadataStore; import org.rakam.config.ProjectConfig; import org.rakam.plugin.MaterializedView; import org.rakam.postgresql.report.PostgresqlQueryExecutor; import org.rakam.report.DelegateQueryExecution; import org.rakam.report.QueryExecution; import org.rakam.report.QueryResult; import org.rakam.util.RakamException; import org.rakam.util.ValidationUtil; import javax.inject.Inject; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import static com.facebook.presto.sql.RakamSqlFormatter.formatSql; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static java.lang.String.format; import static org.rakam.postgresql.report.PostgresqlQueryExecutor.MATERIALIZED_VIEW_PREFIX; import static org.rakam.util.ValidationUtil.checkCollection; import static org.rakam.util.ValidationUtil.checkTableColumn; public class PostgresqlMaterializedViewService extends MaterializedViewService { private final SqlParser parser = new SqlParser(); private final PostgresqlQueryExecutor queryExecutor; private final QueryMetadataStore database; private final ProjectConfig projectConfig; @Inject public PostgresqlMaterializedViewService(ProjectConfig projectConfig, PostgresqlQueryExecutor queryExecutor, QueryMetadataStore database) { super(database, queryExecutor, '"'); this.projectConfig = projectConfig; this.queryExecutor = queryExecutor; this.database = database; } @Override public CompletableFuture<Void> create(String project, MaterializedView materializedView) { materializedView.validateQuery(); StringBuilder builder = new StringBuilder(); Query statement; synchronized (parser) { statement = (Query) parser.createStatement(materializedView.query); } new RakamSqlFormatter.Formatter(builder, name -> queryExecutor .formatTableReference(project, name, Optional.empty(), new HashMap<String, String>() { @Override public String put(String key, String value) { throw new RakamException("Cross database materialized views are not supported in Postgresql deployment type.", BAD_REQUEST); } }, "collection"), '"').process(statement, 1); String format; if(!materializedView.incremental) { format = format("CREATE MATERIALIZED VIEW %s.%s AS %s WITH NO DATA", ValidationUtil.checkProject(project), checkCollection(MATERIALIZED_VIEW_PREFIX + materializedView.tableName), builder.toString()); } else { format = format("CREATE TABLE %s.%s AS %s WITH NO DATA", ValidationUtil.checkProject(project), checkCollection(MATERIALIZED_VIEW_PREFIX + materializedView.tableName), builder.toString()); } QueryResult result = queryExecutor.executeRawStatement(format).getResult().join(); if (result.isFailed()) { throw new RakamException("Couldn't created table: " + result.getError().toString(), BAD_REQUEST); } database.createMaterializedView(project, materializedView); return CompletableFuture.completedFuture(null); } @Override public CompletableFuture<QueryResult> delete(String project, String name) { MaterializedView materializedView = database.getMaterializedView(project, name); database.deleteMaterializedView(project, name); return queryExecutor.executeRawStatement(format("DROP MATERIALIZED VIEW \"%s\".\"%s%s\"", project, MATERIALIZED_VIEW_PREFIX, materializedView.tableName)).getResult(); } @Override public MaterializedViewExecution lockAndUpdateView(String project, MaterializedView materializedView) { CompletableFuture<Instant> f = new CompletableFuture<>(); String tableName = queryExecutor.formatTableReference(project, QualifiedName.of("materialized", materializedView.tableName), Optional.empty(), ImmutableMap.of(), "collection"); Query statement; synchronized (sqlParser) { statement = (Query) sqlParser.createStatement(materializedView.query); } Map<String, String> sessionProperties = new HashMap<>(); if (!materializedView.incremental) { if (!materializedView.needsUpdate(Clock.systemUTC()) || !database.updateMaterializedView(project, materializedView, f)) { return new MaterializedViewExecution(null, tableName); } String collection = checkCollection( MATERIALIZED_VIEW_PREFIX + materializedView.tableName); QueryExecution execution = queryExecutor.executeRawStatement(format("REFRESH MATERIALIZED VIEW %s.%s ", project, collection)); DelegateQueryExecution delegateQueryExecution = new DelegateQueryExecution(execution, result -> { f.complete(!result.isFailed() ? Instant.now() : null); return result; }); return new MaterializedViewExecution(delegateQueryExecution, collection); } else { List<String> referencedCollections = new ArrayList<>(); formatSql(statement, name -> { if (name.getPrefix().map(prefix -> prefix.equals("collection")).orElse(true)) { referencedCollections.add(name.getSuffix()); } return null; }, '"'); String materializedTableReference = tableName; Instant lastUpdated = materializedView.lastUpdate; Instant now = Instant.now(); boolean needsUpdate = lastUpdated == null || Duration .between(now, lastUpdated).compareTo(materializedView.updateInterval) > 0; QueryExecution queryExecution; if (needsUpdate && database.updateMaterializedView(project, materializedView, f)) { String query = formatSql(statement, name -> { String predicate = lastUpdated != null ? format("between timezone('UTC', to_timestamp(%d)) and timezone('UTC', to_timestamp(%d))", lastUpdated.getEpochSecond(), now.getEpochSecond()) : format(" < timezone('UTC', to_timestamp(%d))", now.getEpochSecond()); return format("(SELECT * FROM %s WHERE \"$server_time\" %s) data", queryExecutor.formatTableReference(project, name, Optional.empty(), ImmutableMap.of(), "collection"), predicate); }, '"'); queryExecution = queryExecutor.executeRawStatement(format("INSERT INTO %s %s", materializedTableReference, query), sessionProperties); queryExecution.getResult().thenAccept(result -> f.complete(!result.isFailed() ? now : null)); } else { queryExecution = QueryExecution.completedQueryExecution("", QueryResult.empty()); f.complete(lastUpdated); } String reference; if (!needsUpdate && !materializedView.realTime) { reference = materializedTableReference; } else { String query = formatSql(statement, name -> { String collection = format("(SELECT * FROM %s %s) data", queryExecutor.formatTableReference(project, name, Optional.empty(), ImmutableMap.of(), "collection"), format("WHERE %s > to_timestamp(%d)", checkTableColumn(projectConfig.getTimeColumn()), (lastUpdated != null ? lastUpdated : now).getEpochSecond())); return collection; }, '"'); reference = format("(SELECT * from %s UNION ALL %s) data", materializedTableReference, query); } return new MaterializedViewExecution(queryExecution, reference); } } }