package org.rakam.analysis; import org.rakam.collection.SchemaField; import org.rakam.plugin.MaterializedView; import org.rakam.report.QueryError; import org.rakam.report.QueryExecution; import org.rakam.report.QueryResult; import org.rakam.server.http.HttpService; import org.rakam.server.http.RakamHttpRequest; import org.rakam.server.http.annotations.Api; import org.rakam.server.http.annotations.ApiOperation; import org.rakam.server.http.annotations.ApiParam; import org.rakam.server.http.annotations.Authorization; import org.rakam.server.http.annotations.BodyParam; import org.rakam.server.http.annotations.IgnoreApi; import org.rakam.server.http.annotations.JsonRequest; import org.rakam.util.RakamException; import org.rakam.util.SuccessMessage; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; @Path("/materialized-view") @Api(value = "/materialized-view", nickname = "materializedView", description = "Materialized View", tags = "materialized-view") public class MaterializedViewHttpService extends HttpService { private final MaterializedViewService service; private final QueryHttpService queryService; @Inject public MaterializedViewHttpService(MaterializedViewService service, QueryHttpService queryService) { this.service = service; this.queryService = queryService; } @JsonRequest @ApiOperation(value = "List views", authorizations = @Authorization(value = "read_key")) @Path("/list") public List<MaterializedView> listViews(@javax.inject.Named("project") String project) { return service.list(project); } @JsonRequest @ApiOperation(value = "Get schemas", authorizations = @Authorization(value = "read_key")) @Path("/schema") public List<MaterializedViewSchema> getSchemaOfView(@javax.inject.Named("project") String project, @ApiParam(value = "names", required = false) List<String> tableNames) { return service.getSchemas(project, Optional.ofNullable(tableNames)).entrySet().stream() .map(entry -> new MaterializedViewSchema(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } public static class MaterializedViewSchema { public final String name; public final List<SchemaField> fields; public MaterializedViewSchema(String name, List<SchemaField> fields) { this.name = name; this.fields = fields; } } /** * Creates a new materialized view for specified SQL query. * materialized views allow you to execute batch queries over the data-set. * Rakam caches the materialized view result and serve the cached data when you request. * You can also trigger an update using using '/view/update' endpoint. * This feature is similar to MATERIALIZED VIEWS in RDBMSs. * <p> * curl 'http://localhost:9999/materialized-view/create' -H 'Content-Type: text/event-stream;charset=UTF-8' --data-binary '{"project": "projectId", "name": "Yearly Visits", "query": "SELECT year(time), count(1) from visits GROUP BY 1"}' * * @param query materialized view query * @return the status */ @JsonRequest @ApiOperation(value = "Create view", authorizations = @Authorization(value = "master_key")) @Path("/create") public CompletableFuture<SuccessMessage> createView(@javax.inject.Named("project") String project, @BodyParam MaterializedView query) { return service.create(project, query).thenApply(res -> SuccessMessage.success()); } @JsonRequest @ApiOperation(value = "Delete materialized view", authorizations = @Authorization(value = "master_key")) @Path("/delete") public CompletableFuture<SuccessMessage> deleteView(@javax.inject.Named("project") String project, @ApiParam("table_name") String name) { return service.delete(project, name) .thenApply(result -> { if(result.getError() == null) { return SuccessMessage.success(); } else { throw new RakamException(result.getError().message, INTERNAL_SERVER_ERROR); } }); } @GET @Consumes("text/event-stream") @Path("/update") @ApiOperation(value = "Update view", authorizations = @Authorization(value = "master_key"), notes = "Invalidate previous cached data, executes the materialized view query and caches it.\n" + "This feature is similar to UPDATE MATERIALIZED VIEWS in RDBMSs.") @IgnoreApi public void update(RakamHttpRequest request) { queryService.handleServerSentQueryExecution(request, MaterializedViewRequest.class, (project, query) -> { QueryExecution execution = service.lockAndUpdateView(project, service.get(project, query.name)).queryExecution; if (execution == null) { QueryResult result = QueryResult.errorResult(new QueryError("There is another process that updates materialized view", null, null, null, null)); return QueryExecution.completedQueryExecution(null, result); } return execution; }); } public static class MaterializedViewRequest { public final String name; public MaterializedViewRequest(String name) { this.name = name; } } @JsonRequest @ApiOperation(value = "Get view", authorizations = @Authorization(value = "read_key")) @Path("/get") public MaterializedView getView(@javax.inject.Named("project") String project, @ApiParam("table_name") String tableName) { return service.get(project, tableName); } }