package org.rakam.postgresql.plugin.user; import com.facebook.presto.sql.tree.QualifiedName; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; 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.plugin.user.AbstractUserService; import org.rakam.plugin.user.ISingleUserBatchOperation; import org.rakam.postgresql.report.PostgresqlQueryExecutor; import org.rakam.report.QueryExecution; import org.rakam.report.QueryResult; import org.rakam.util.JsonHelper; import org.rakam.util.RakamException; import javax.inject.Inject; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static org.rakam.util.ValidationUtil.checkCollection; import static org.rakam.util.ValidationUtil.checkLiteral; import static org.rakam.util.ValidationUtil.checkProject; import static org.rakam.util.ValidationUtil.checkTableColumn; public class PostgresqlUserService extends AbstractUserService { private final Metastore metastore; private final PostgresqlQueryExecutor executor; private final PostgresqlUserStorage storage; private final ProjectConfig projectConfig; @Inject public PostgresqlUserService(ProjectConfig projectConfig, PostgresqlUserStorage storage, Metastore metastore, PostgresqlQueryExecutor executor) { super(storage); this.storage = storage; this.projectConfig = projectConfig; this.metastore = metastore; this.executor = executor; } @Override public CompletableFuture<List<CollectionEvent>> getEvents(String project, String user, Optional<List<String>> properties, int limit, Instant beforeThisTime) { checkProject(project); checkNotNull(user); checkArgument(limit <= 1000, "Maximum 1000 events can be fetched at once."); String sqlQuery = metastore.getCollections(project).entrySet().stream() .filter(entry -> entry.getValue().stream().anyMatch(field -> field.getName().equals(projectConfig.getUserColumn()))) .filter(entry -> entry.getValue().stream().anyMatch(field -> field.getName().equals(projectConfig.getTimeColumn()))) .map(entry -> format("select '%s' as collection, row_to_json(coll) json, %s from %s.%s coll where _user = '%s' %s", entry.getKey(), checkTableColumn(projectConfig.getTimeColumn()), checkCollection(project), checkCollection(entry.getKey()), checkLiteral(user), beforeThisTime == null ? "" : format("and %s < timestamp '%s'", checkTableColumn(projectConfig.getTimeColumn()), beforeThisTime.toString()))) .collect(Collectors.joining(" union all ")); if (sqlQuery.isEmpty()) { return CompletableFuture.completedFuture(ImmutableList.<CollectionEvent>of()); } CompletableFuture<QueryResult> queryResult = executor.executeRawQuery(format("select collection, json from (%s) data order by %s desc limit %d", sqlQuery, checkTableColumn(projectConfig.getTimeColumn()), limit)).getResult(); return queryResult.thenApply(result -> { if (result.isFailed()) { throw new RakamException(result.getError().toString(), HttpResponseStatus.INTERNAL_SERVER_ERROR); } List<CollectionEvent> events = new ArrayList(result.getResult().size()); events.addAll(result.getResult().stream() .map(objects -> { Map<String, Object> read = JsonHelper.read(objects.get(1).toString(), Map.class); return new CollectionEvent((String) objects.get(0), read); }) .collect(Collectors.toList())); return events; }); } @Override public void merge(String project, Object user, Object anonymousId, Instant createdAt, Instant mergedAt) { for (Map.Entry<String, List<SchemaField>> entry : metastore.getCollections(project).entrySet()) { if (!entry.getValue().stream().anyMatch(e -> e.getName().equals("_user")) || !entry.getValue().stream().anyMatch(e -> e.getName().equals("_device_id"))) { continue; } try (Connection connection = executor.getConnection()) { PreparedStatement ps = connection.prepareStatement(format("UPDATE %s SET _user = ? WHERE _device_id = ? AND _user is NULL AND %s BETWEEN ? and ?", executor.formatTableReference(project, QualifiedName.of(entry.getKey()), Optional.empty(), ImmutableMap.of(), "collection"), checkTableColumn(projectConfig.getTimeColumn()))); storage.setUserId(project, ps, user, 1); storage.setUserId(project, ps, anonymousId, 2); ps.setTimestamp(3, Timestamp.from(createdAt)); ps.setTimestamp(4, Timestamp.from(mergedAt)); ps.executeUpdate(); } catch (SQLException e) { throw Throwables.propagate(e); } } } @Override public QueryExecution preCalculate(String project, PreCalculateQuery query) { // no-op return QueryExecution.completedQueryExecution(null, QueryResult.empty()); } @Override public void batch(String project, List<? extends ISingleUserBatchOperation> batchUserOperations) { storage.batch(project, batchUserOperations); } }