This is an automated email from the ASF dual-hosted git repository. progers pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/drill.git
The following commit(s) were added to refs/heads/master by this push: new f98ab9f DRILL-7603 and DRILL-7604: Add schema, options to REST query f98ab9f is described below commit f98ab9ff1d3159f966c9795a309622987e9a905a Author: Paul Rogers <par0...@yahoo.com> AuthorDate: Fri Apr 10 12:45:12 2020 -0700 DRILL-7603 and DRILL-7604: Add schema, options to REST query Update and revision of work originally done by dobesv. DRILL-7603: Allow default schema to be set for HTTP queries DRILL-7604: Allow session options to be set in HTTP queries Merges the above two. Separates running a REST query from the JSON representation. Allows setting all option types from a string (as required by DRILL-7604). Added default schema to query profile query editor. Made the two query editors a bit more similar visually, but see DRILL-7697 for more work needed. Added a utility to run a server for UI teseting without a full build. --- .../java/org/apache/drill/exec/ExecConstants.java | 3 +- .../exec/server/options/BaseOptionManager.java | 27 +-- .../exec/server/options/FallbackOptionManager.java | 10 +- .../exec/server/options/InMemoryOptionManager.java | 9 +- .../drill/exec/server/options/OptionValue.java | 98 ++++++-- .../exec/server/options/SessionOptionManager.java | 1 - .../apache/drill/exec/server/rest/DrillRoot.java | 6 +- .../drill/exec/server/rest/QueryResources.java | 49 +++- .../drill/exec/server/rest/QueryWrapper.java | 258 ++++++++------------- .../{QueryWrapper.java => RestQueryRunner.java} | 166 ++++++------- .../apache/drill/exec/server/rest/WebServer.java | 22 +- .../exec/server/rest/profile/ProfileResources.java | 12 +- .../src/main/resources/rest/profile/profile.ftl | 53 +++-- .../src/main/resources/rest/query/query.ftl | 51 ++-- .../drill/exec/server/options/OptionValueTest.java | 32 ++- .../drill/exec/server/rest/InteractiveUI.java | 43 ++++ .../drill/exec/server/rest/RestServerTest.java | 24 +- .../drill/exec/server/rest/TestQueryWrapper.java | 101 +++++++- .../server/rest/TestQueryWrapperImpersonation.java | 9 +- .../java/org/apache/drill/test/ClusterFixture.java | 5 + .../apache/drill/test/ClusterFixtureBuilder.java | 13 ++ 21 files changed, 601 insertions(+), 391 deletions(-) diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java index 027ebb5..a0d14a9 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java @@ -472,7 +472,8 @@ public final class ExecConstants { public static final String JSON_READER_PRINT_INVALID_RECORDS_LINE_NOS_FLAG = "store.json.reader.print_skipped_invalid_record_number"; public static final BooleanValidator JSON_READER_PRINT_INVALID_RECORDS_LINE_NOS_FLAG_VALIDATOR = new BooleanValidator(JSON_READER_PRINT_INVALID_RECORDS_LINE_NOS_FLAG, new OptionDescription("Enables Drill to log the bad records that the JSON record reader skips when reading JSON files. Default is false. (Drill 1.9+)")); - public static final DoubleValidator TEXT_ESTIMATED_ROW_SIZE = new RangeDoubleValidator("store.text.estimated_row_size_bytes", 1, Long.MAX_VALUE, + public static final String TEXT_ESTIMATED_ROW_SIZE_KEY = "store.text.estimated_row_size_bytes"; + public static final DoubleValidator TEXT_ESTIMATED_ROW_SIZE = new RangeDoubleValidator(TEXT_ESTIMATED_ROW_SIZE_KEY, 1, Long.MAX_VALUE, new OptionDescription("Estimate of the row size in a delimited text file, such as csv. The closer to actual, the better the query plan. Used for all csv files in the system/session where the value is set. Impacts the decision to plan a broadcast join or not.")); public static final String TEXT_WRITER_ADD_HEADER = "store.text.writer.add_header"; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/BaseOptionManager.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/BaseOptionManager.java index ee786c0..6b8ea80 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/BaseOptionManager.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/BaseOptionManager.java @@ -19,6 +19,8 @@ package org.apache.drill.exec.server.options; import org.apache.drill.common.exceptions.UserException; import org.apache.drill.exec.server.options.OptionValue.Kind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Iterator; @@ -28,7 +30,7 @@ import java.util.Iterator; */ public abstract class BaseOptionManager implements OptionManager { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(BaseOptionManager.class); + private static final Logger logger = LoggerFactory.getLogger(BaseOptionManager.class); /** * Gets the current option value given a validator. @@ -142,33 +144,14 @@ public abstract class BaseOptionManager implements OptionManager { final OptionValue.AccessibleScopes type = definition.getMetaData().getAccessibleScopes(); final OptionValue.OptionScope scope = getScope(); checkOptionPermissions(name, type, scope); - final OptionValue optionValue = OptionValue.create(type, name, value, scope); + final OptionValue optionValue = OptionValue.create(type, name, value, scope, validator.getKind()); validator.validate(optionValue, metaData, this); setLocalOptionHelper(optionValue); } @Override public void setLocalOption(final OptionValue.Kind kind, final String name, final String valueStr) { - Object value; - - switch (kind) { - case LONG: - value = Long.valueOf(valueStr); - break; - case DOUBLE: - value = Double.valueOf(valueStr); - break; - case STRING: - value = valueStr; - break; - case BOOLEAN: - value = Boolean.valueOf(valueStr); - break; - default: - throw new IllegalArgumentException(String.format("Unsupported kind %s", kind)); - } - - setLocalOption(name, value); + setLocalOption(name, valueStr); } private static void checkOptionPermissions(String name, OptionValue.AccessibleScopes type, OptionValue.OptionScope scope) { diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/FallbackOptionManager.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/FallbackOptionManager.java index e7bb476..be90f44 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/FallbackOptionManager.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/FallbackOptionManager.java @@ -22,13 +22,15 @@ import java.util.Iterator; import org.apache.drill.shaded.guava.com.google.common.collect.Iterables; /** - * An {@link OptionManager} which allows for falling back onto another {@link OptionManager} when retrieving options. + * An {@link OptionManager} which allows for falling back onto another + * {@link OptionManager} when retrieving options. * <p/> - * {@link FragmentOptionManager} and {@link SessionOptionManager} use {@link SystemOptionManager} as the fall back - * manager. {@link QueryOptionManager} uses {@link SessionOptionManager} as the fall back manager. + * {@link FragmentOptionManager} and {@link SessionOptionManager} use + * {@link SystemOptionManager} as the fall back manager. + * {@link QueryOptionManager} uses {@link SessionOptionManager} as the fall back + * manager. */ public abstract class FallbackOptionManager extends BaseOptionManager { -// private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(FallbackOptionManager.class); protected final OptionManager fallback; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/InMemoryOptionManager.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/InMemoryOptionManager.java index c3cd63d..78dab55 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/InMemoryOptionManager.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/InMemoryOptionManager.java @@ -20,12 +20,13 @@ package org.apache.drill.exec.server.options; import java.util.Map; /** - * This is an {@link OptionManager} that holds options in memory rather than in a persistent store. Options stored in - * {@link SessionOptionManager}, {@link QueryOptionManager}, and {@link FragmentOptionManager} are held in memory - * (see {@link #options}) whereas the {@link SystemOptionManager} stores options in a persistent store. + * This is an {@link OptionManager} that holds options in memory rather than in + * a persistent store. Options stored in {@link SessionOptionManager}, + * {@link QueryOptionManager}, and {@link FragmentOptionManager} are held in + * memory (see {@link #options}) whereas the {@link SystemOptionManager} stores + * options in a persistent store. */ public abstract class InMemoryOptionManager extends FallbackOptionManager { -// private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InMemoryOptionManager.class); protected final Map<String, OptionValue> options; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/OptionValue.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/OptionValue.java index 61d430d..e74f46a 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/OptionValue.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/OptionValue.java @@ -22,27 +22,38 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; + +import org.apache.drill.common.exceptions.UserException; import org.apache.drill.shaded.guava.com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.EnumSet; /** * <p> - * An {@link OptionValue option value} is used internally by an {@link OptionManager} to store a run-time setting. This setting, - * for example, could affect a query in an execution stage. Instances of this class are JSON serializable and can be stored - * in a {@link PersistentStore persistent store} (see {@link SystemOptionManager#options}), or - * in memory (see {@link InMemoryOptionManager#options}). + * An {@link OptionValue option value} is used internally by an + * {@link OptionManager} to store a run-time setting. This setting, for example, + * could affect a query in an execution stage. Instances of this class are JSON + * serializable and can be stored in a {@link PersistentStore persistent store} + * (see {@link SystemOptionManager#options}), or in memory (see + * {@link InMemoryOptionManager#options}). * </p> * <p> - * {@link AccessibleScopes} defines the scopes at which the option can be set. If it can be set at System level or Session level or so on. - * Whereas {@link OptionScope} defines the scope at which the option is being set. If the option is being set at the BOOT time - * the scope of the option is BOOT. If it is set at SYSTEM level the scope is SYSTEM. Although they look similar there - * is a fine level which differentiates both of them which is at which level of hierarchy they can be set and - * at what at level of hierarchy they were actually set. + * {@link AccessibleScopes} defines the scopes at which the option can be set. + * If it can be set at System level or Session level or so on. Whereas + * {@link OptionScope} defines the scope at which the option is being set. If + * the option is being set at the BOOT time the scope of the option is BOOT. If + * it is set at SYSTEM level the scope is SYSTEM. Although they look similar + * there is a fine level which differentiates both of them which is at which + * level of hierarchy they can be set and at what at level of hierarchy they + * were actually set. * </p> */ @JsonInclude(Include.NON_NULL) public class OptionValue implements Comparable<OptionValue> { + private static final Logger logger = LoggerFactory.getLogger(OptionValue.class); + public static final String JSON_KIND = "kind"; public static final String JSON_ACCESSIBLE_SCOPES = "accessibleScopes"; public static final String JSON_NAME = "name"; @@ -114,21 +125,29 @@ public class OptionValue implements Comparable<OptionValue> { public static OptionValue create(Kind kind, AccessibleScopes accessibleScopes, String name, String val, OptionScope scope) { - switch (kind) { - case BOOLEAN: - return create(accessibleScopes, name, Boolean.valueOf(val), scope); - case STRING: - return create(accessibleScopes, name, val, scope); - case DOUBLE: - return create(accessibleScopes, name, Double.parseDouble(val), scope); - case LONG: - return create(accessibleScopes, name, Long.parseLong(val), scope); - default: - throw new IllegalArgumentException(String.format("Unsupported kind %s", kind)); + try { + switch (kind) { + case BOOLEAN: + return create(accessibleScopes, name, Boolean.valueOf(val), scope); + case STRING: + return create(accessibleScopes, name, val, scope); + case DOUBLE: + return create(accessibleScopes, name, Double.parseDouble(val), scope); + case LONG: + return create(accessibleScopes, name, Long.parseLong(val), scope); + default: + throw new IllegalArgumentException(String.format("Unsupported kind %s", kind)); + } + } catch (NumberFormatException e) { + throw UserException.validationError(e) + .message("'%s' is not a valid value option '%s' of type %s", + val.toString(), name, kind.name()) + .build(logger); } } - public static OptionValue create(AccessibleScopes type, String name, Object val, OptionScope scope) { + public static OptionValue create(AccessibleScopes type, String name, Object val, OptionScope scope, Kind kind) { + Preconditions.checkArgument(val != null); if (val instanceof Boolean) { return create(type, name, ((Boolean) val).booleanValue(), scope); } else if (val instanceof Long) { @@ -136,7 +155,7 @@ public class OptionValue implements Comparable<OptionValue> { } else if (val instanceof Integer) { return create(type, name, ((Integer) val).longValue(), scope); } else if (val instanceof String) { - return create(type, name, (String) val, scope); + return fromString(type, name, (String) val, scope, kind); } else if (val instanceof Double) { return create(type, name, ((Double) val).doubleValue(), scope); } else if (val instanceof Float) { @@ -146,6 +165,41 @@ public class OptionValue implements Comparable<OptionValue> { throw new IllegalArgumentException(String.format("Unsupported type %s", val.getClass())); } + private static OptionValue fromString(AccessibleScopes type, String name, + String val, OptionScope scope, Kind kind) { + Preconditions.checkArgument(val != null); + val = val.trim(); + try { + switch (kind) { + case BOOLEAN: { + + // Strict enforcement of true or false, in any case + val = val.toLowerCase(); + if (!val.equals("true") && !val.equals("false")) { + throw UserException.validationError() + .message("'%s' is not a valid value for option '%s' of type %s", + val, name, kind.name()) + .build(logger); + } + return create(type, name, Boolean.parseBoolean(val.toLowerCase()), scope); + } + case DOUBLE: + return create(type, name, Double.parseDouble(val), scope); + case LONG: + return create(type, name, Long.parseLong(val), scope); + case STRING: + return create(type, name, val, scope); + default: + throw new IllegalStateException(kind.name()); + } + } catch (NumberFormatException e) { + throw UserException.validationError(e) + .message("'%s' is not a valid value for option '%s' of type %s", + val, name, kind.name()) + .build(logger); + } + } + @JsonCreator private OptionValue(@JsonProperty(JSON_KIND) Kind kind, @JsonProperty(JSON_ACCESSIBLE_SCOPES) AccessibleScopes accessibleScopes, diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/SessionOptionManager.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/SessionOptionManager.java index f7da53c..d719567 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/SessionOptionManager.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/options/SessionOptionManager.java @@ -38,7 +38,6 @@ import org.apache.drill.shaded.guava.com.google.common.collect.Collections2; * in the reset query itself. */ public class SessionOptionManager extends InMemoryOptionManager { -// private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SessionOptionManager.class); private final UserSession session; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRoot.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRoot.java index 1910737..a912719 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRoot.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRoot.java @@ -58,6 +58,8 @@ import org.apache.drill.exec.work.foreman.rm.ResourceManager; import org.apache.drill.exec.work.foreman.rm.ThrottledResourceManager; import org.apache.http.client.methods.HttpPost; import org.glassfish.jersey.server.mvc.Viewable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonCreator; @@ -66,7 +68,7 @@ import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_RO @Path("/") @PermitAll public class DrillRoot { - static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillRoot.class); + private static final Logger logger = LoggerFactory.getLogger(DrillRoot.class); @Inject UserAuthEnabled authEnabled; @@ -234,7 +236,7 @@ public class DrillRoot { endpoint1.getUserPort() == endpoint2.getUserPort(); } - private Response setResponse(Map entity) { + private Response setResponse(Map<String, ?> entity) { return Response.ok() .entity(entity) .header("Access-Control-Allow-Origin", "*") diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java index e1fc811..6e6005e 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java @@ -17,15 +17,15 @@ */ package org.apache.drill.exec.server.rest; -import org.apache.drill.shaded.guava.com.google.common.base.CharMatcher; import org.apache.drill.shaded.guava.com.google.common.base.Joiner; import org.apache.drill.shaded.guava.com.google.common.collect.ImmutableList; import org.apache.drill.shaded.guava.com.google.common.collect.Lists; import org.apache.drill.common.config.DrillConfig; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.server.rest.DrillRestServer.UserAuthEnabled; +import org.apache.drill.exec.server.rest.QueryWrapper.RestQueryBuilder; +import org.apache.drill.exec.server.rest.RestQueryRunner.QueryResult; import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal; -import org.apache.drill.exec.server.rest.QueryWrapper.QueryResult; import org.apache.drill.exec.work.WorkManager; import org.glassfish.jersey.server.mvc.Viewable; import org.slf4j.Logger; @@ -34,15 +34,18 @@ import org.slf4j.LoggerFactory; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -90,7 +93,7 @@ public class QueryResources { public QueryResult submitQueryJSON(QueryWrapper query) throws Exception { try { // Run the query - return query.run(work, webUserConnection); + return new RestQueryRunner(query, work, webUserConnection).run(); } finally { // no-op for authenticated user webUserConnection.cleanupSession(); @@ -105,12 +108,20 @@ public class QueryResources { @FormParam("queryType") String queryType, @FormParam("autoLimit") String autoLimit, @FormParam("userName") String userName, - @FormParam("defaultSchema") String defaultSchema) throws Exception { + @FormParam("defaultSchema") String defaultSchema, + Form form) throws Exception { try { - // Apply options from the form fields, if provided - final String trimmedQueryString = CharMatcher.is(';').trimTrailingFrom(query.trim()); - final QueryResult result = submitQueryJSON(new QueryWrapper(trimmedQueryString, queryType, autoLimit, userName, defaultSchema)); - List<Integer> rowsPerPageValues = work.getContext().getConfig().getIntList(ExecConstants.HTTP_WEB_CLIENT_RESULTSET_ROWS_PER_PAGE_VALUES); + final QueryResult result = submitQueryJSON( + new RestQueryBuilder() + .query(query) + .queryType(queryType) + .rowLimit(autoLimit) + .userName(userName) + .defaultSchema(defaultSchema) + .sessionOptions(readOptionsFromForm(form)) + .build()); + List<Integer> rowsPerPageValues = work.getContext().getConfig().getIntList( + ExecConstants.HTTP_WEB_CLIENT_RESULTSET_ROWS_PER_PAGE_VALUES); Collections.sort(rowsPerPageValues); final String rowsPerPageValuesAsStr = Joiner.on(",").join(rowsPerPageValues); return ViewableWithPermissions.create(authEnabled.get(), "/rest/query/result.ftl", sc, new TabularResult(result, rowsPerPageValuesAsStr)); @@ -121,6 +132,27 @@ public class QueryResources { } /** + * Convert the form to a map. The form allows multiple values per key; + * discard the entry if empty, throw an error if more than one value. + */ + private Map<String, String> readOptionsFromForm(Form form) { + Map<String, String> options = new HashMap<>(); + for (Map.Entry<String, List<String>> pair : form.asMap().entrySet()) { + List<String> values = pair.getValue(); + if (values.isEmpty()) { + continue; + } + if (values.size() > 1) { + throw new BadRequestException(String.format( + "Multiple values given for option '%s'", pair.getKey())); + } + + options.put(pair.getKey(), values.get(0)); + } + return options; + } + + /** * Model class for Query page */ public static class QueryPage { @@ -225,5 +257,4 @@ public class QueryResources { return autoLimitedRowCount; } } - } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java index 0a7bcd7..94fcea9 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java @@ -17,46 +17,38 @@ */ package org.apache.drill.exec.server.rest; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.calcite.schema.SchemaPlus; -import org.apache.drill.common.config.DrillConfig; -import org.apache.drill.common.exceptions.UserException; -import org.apache.drill.common.exceptions.UserRemoteException; -import org.apache.drill.exec.ExecConstants; -import org.apache.drill.exec.proto.UserBitShared; -import org.apache.drill.exec.proto.UserBitShared.QueryId; -import org.apache.drill.exec.proto.UserBitShared.QueryResult.QueryState; -import org.apache.drill.exec.proto.UserBitShared.QueryType; -import org.apache.drill.exec.proto.UserProtos.QueryResultsMode; -import org.apache.drill.exec.proto.UserProtos.RunQuery; -import org.apache.drill.exec.proto.helper.QueryIdHelper; -import org.apache.drill.exec.rpc.user.InboundImpersonationManager; -import org.apache.drill.exec.server.options.SessionOptionManager; -import org.apache.drill.exec.store.SchemaTreeProvider; -import org.apache.drill.exec.util.ImpersonationUtil; -import org.apache.drill.exec.work.WorkManager; -import org.apache.parquet.Strings; +import java.util.Map; import javax.xml.bind.annotation.XmlRootElement; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; + +import org.apache.drill.common.PlanStringBuilder; +import org.apache.drill.shaded.guava.com.google.common.base.CharMatcher; +import org.apache.drill.shaded.guava.com.google.common.base.Preconditions; +import org.apache.parquet.Strings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; @XmlRootElement public class QueryWrapper { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(QueryWrapper.class); private final String query; private final String queryType; - private final int autoLimitRowCount; - private final String userName; - private final String defaultSchema; - - private static MemoryMXBean memMXBean = ManagementFactory.getMemoryMXBean(); + final int autoLimitRowCount; + final String userName; + final String defaultSchema; + final Map<String, String> options; + + protected QueryWrapper(String query, String queryType, + int rowCountLimit, String userName, String defaultSchema, + Map<String, String> options) { + this.query = query; + this.queryType = queryType.toUpperCase(); + this.autoLimitRowCount = rowCountLimit; + this.userName = userName; + this.defaultSchema = defaultSchema; + this.options = options; + } @JsonCreator public QueryWrapper( @@ -64,152 +56,98 @@ public class QueryWrapper { @JsonProperty("queryType") String queryType, @JsonProperty("autoLimit") String autoLimit, @JsonProperty("userName") String userName, - @JsonProperty("defaultSchema") String defaultSchema) { - this.query = query; - this.queryType = queryType.toUpperCase(); - this.autoLimitRowCount = autoLimit != null && autoLimit.matches("[0-9]+") ? Integer.valueOf(autoLimit) : 0; - this.userName = userName; - this.defaultSchema = defaultSchema; + @JsonProperty("defaultSchema") String defaultSchema, + @JsonProperty("options") Map<String, String> options) { + this(query, queryType, mapCount(autoLimit), userName, defaultSchema, options); } - public String getQuery() { - return query; + private static int mapCount(String rowLimit) { + if (Strings.isNullOrEmpty(rowLimit)) { + return 0; + } + try { + return Integer.parseInt(rowLimit.trim()); + } catch (NumberFormatException e) { + return 0; + } } - public String getQueryType() { - return queryType; - } + public String getQuery() { return query; } + + public String getQueryType() { return queryType; } + + public String getUserName() { return userName; } + + public int getAutoLimitRowCount() { return autoLimitRowCount; } + + public String getDefaultSchema() { return defaultSchema; } + + public Map<String, String> getOptions() { return options; } - public QueryType getType() { - return QueryType.valueOf(queryType); + @Override + public String toString() { + return new PlanStringBuilder(this) + .field("query", query) + .field("query type", queryType) + .field("user name", userName) + .field("default schema", defaultSchema) + .field("row limit", autoLimitRowCount) + .toString(); } - public QueryResult run(final WorkManager workManager, final WebUserConnection webUserConnection) throws Exception { - final RunQuery runQuery = RunQuery.newBuilder().setType(getType()) - .setPlan(getQuery()) - .setResultsMode(QueryResultsMode.STREAM_FULL) - .setAutolimitRowcount(autoLimitRowCount) - .build(); - - applyUserName(workManager, webUserConnection); - - int defaultMaxRows = webUserConnection.getSession().getOptions().getOption(ExecConstants.QUERY_MAX_ROWS).num_val.intValue(); - if (!Strings.isNullOrEmpty(defaultSchema)) { - SessionOptionManager options = webUserConnection.getSession().getOptions(); - SchemaTreeProvider schemaTreeProvider = new SchemaTreeProvider(workManager.getContext()); - SchemaPlus rootSchema = schemaTreeProvider.createRootSchema(options); - webUserConnection.getSession().setDefaultSchemaPath(defaultSchema, rootSchema); - } + public static final class RestQueryBuilder { - int maxRows; - if (autoLimitRowCount > 0 && defaultMaxRows > 0) { - maxRows = Math.min(autoLimitRowCount, defaultMaxRows); - } else { - maxRows = Math.max(autoLimitRowCount, defaultMaxRows); - } - webUserConnection.setAutoLimitRowCount(maxRows); - - // Heap usage threshold/trigger to provide resiliency on web server for queries submitted via HTTP - double memoryFailureThreshold = workManager.getContext().getConfig().getDouble(ExecConstants.HTTP_MEMORY_HEAP_FAILURE_THRESHOLD); - - // Submit user query to Drillbit work queue. - final QueryId queryId = workManager.getUserWorker().submitWork(webUserConnection, runQuery); - - boolean isComplete = false; - boolean nearlyOutOfHeapSpace = false; - float usagePercent = getHeapUsage(); - - // Wait until the query execution is complete or there is error submitting the query - logger.debug("Wait until the query execution is complete or there is error submitting the query"); - do { - try { - isComplete = webUserConnection.await(TimeUnit.SECONDS.toMillis(1)); //periodically timeout 1 sec to check heap - } catch (InterruptedException e) {} - usagePercent = getHeapUsage(); - if (memoryFailureThreshold > 0 && usagePercent > memoryFailureThreshold) { - nearlyOutOfHeapSpace = true; - } - } while (!isComplete && !nearlyOutOfHeapSpace); - - //Fail if nearly out of heap space - if (nearlyOutOfHeapSpace) { - UserException almostOutOfHeapException = UserException.resourceError() - .message("There is not enough heap memory to run this query using the web interface. ") - .addContext("Please try a query with fewer columns or with a filter or limit condition to limit the data returned. ") - .addContext("You can also try an ODBC/JDBC client. ") - .build(logger); - //Add event - workManager.getBee().getForemanForQueryId(queryId) - .addToEventQueue(QueryState.FAILED, almostOutOfHeapException); - //Return NearlyOutOfHeap exception - throw almostOutOfHeapException; + private String query; + private String queryType = "SQL"; + private int rowLimit; + private String userName; + private String defaultSchema; + private Map<String, String> options; + + public RestQueryBuilder query(String query) { + this.query = query; + return this; } - logger.trace("Query {} is completed ", queryId); + public RestQueryBuilder queryType(String queryType) { + this.queryType = queryType; + return this; + } - if (webUserConnection.getError() != null) { - throw new UserRemoteException(webUserConnection.getError()); + public RestQueryBuilder rowLimit(int rowLimit) { + this.rowLimit = rowLimit; + return this; } - // Return the QueryResult. - return new QueryResult(queryId, webUserConnection, webUserConnection.results); - } + public RestQueryBuilder rowLimit(String rowLimit) { + this.rowLimit = mapCount(rowLimit); + return this; + } - private void applyUserName(WorkManager workManager, WebUserConnection webUserConnection) { - SessionOptionManager options = webUserConnection.getSession().getOptions(); - DrillConfig config = workManager.getContext().getConfig(); - if (!Strings.isNullOrEmpty(userName)) { - if (!config.getBoolean(ExecConstants.IMPERSONATION_ENABLED)) { - throw UserException.permissionError().message("User impersonation is not enabled").build(logger); - } - InboundImpersonationManager inboundImpersonationManager = new InboundImpersonationManager(); - boolean isAdmin = !config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED) || - ImpersonationUtil.hasAdminPrivileges(webUserConnection.getSession().getCredentials().getUserName(), - ExecConstants.ADMIN_USERS_VALIDATOR.getAdminUsers(options), - ExecConstants.ADMIN_USER_GROUPS_VALIDATOR.getAdminUserGroups(options)); - if (isAdmin) { - // Admin user can impersonate any user they want to (when authentication is disabled, all users are admin) - webUserConnection.getSession().replaceUserCredentials( - inboundImpersonationManager, - UserBitShared.UserCredentials.newBuilder().setUserName(userName).build()); - } else { - // Check configured impersonation rules to see if this user is allowed to impersonate the given user - inboundImpersonationManager.replaceUserOnSession(userName, webUserConnection.getSession()); - } + public RestQueryBuilder userName(String userName) { + this.userName = userName; + return this; } - } - //Detect possible excess heap - private float getHeapUsage() { - return (float) memMXBean.getHeapMemoryUsage().getUsed() / memMXBean.getHeapMemoryUsage().getMax(); - } + public RestQueryBuilder defaultSchema(String defaultSchema) { + this.defaultSchema = defaultSchema; + return this; + } - public static class QueryResult { - private final String queryId; - public final Collection<String> columns; - public final List<Map<String, String>> rows; - public final List<String> metadata; - public final String queryState; - public final int attemptedAutoLimit; - - //DRILL-6847: Modified the constructor so that the method has access to all the properties in webUserConnection - public QueryResult(QueryId queryId, WebUserConnection webUserConnection, List<Map<String, String>> rows) { - this.queryId = QueryIdHelper.getQueryId(queryId); - this.columns = webUserConnection.columns; - this.metadata = webUserConnection.metadata; - this.queryState = webUserConnection.getQueryState(); - this.rows = rows; - this.attemptedAutoLimit = webUserConnection.getAutoLimitRowCount(); - } - - public String getQueryId() { - return queryId; + /** + * Optional session option values encoded as strings. + */ + public RestQueryBuilder sessionOptions(Map<String, String> options) { + this.options = options; + return this; } - } - @Override - public String toString() { - return "QueryRequest [queryType=" + queryType + ", query=" + query + "]"; + public QueryWrapper build() { + Preconditions.checkArgument(!Strings.isNullOrEmpty(query)); + query = CharMatcher.is(';').trimTrailingFrom(query.trim()); + Preconditions.checkArgument(!query.isEmpty()); + return new QueryWrapper(query, queryType, rowLimit, userName, + defaultSchema, options); + } } - } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/RestQueryRunner.java similarity index 72% copy from exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java copy to exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/RestQueryRunner.java index 0a7bcd7..e944386 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/RestQueryRunner.java @@ -17,17 +17,23 @@ */ package org.apache.drill.exec.server.rest; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.tools.ValidationException; import org.apache.drill.common.config.DrillConfig; import org.apache.drill.common.exceptions.UserException; import org.apache.drill.common.exceptions.UserRemoteException; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.proto.UserBitShared; import org.apache.drill.exec.proto.UserBitShared.QueryId; -import org.apache.drill.exec.proto.UserBitShared.QueryResult.QueryState; import org.apache.drill.exec.proto.UserBitShared.QueryType; +import org.apache.drill.exec.proto.UserBitShared.QueryResult.QueryState; import org.apache.drill.exec.proto.UserProtos.QueryResultsMode; import org.apache.drill.exec.proto.UserProtos.RunQuery; import org.apache.drill.exec.proto.helper.QueryIdHelper; @@ -37,77 +43,101 @@ import org.apache.drill.exec.store.SchemaTreeProvider; import org.apache.drill.exec.util.ImpersonationUtil; import org.apache.drill.exec.work.WorkManager; import org.apache.parquet.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.xml.bind.annotation.XmlRootElement; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; +public class RestQueryRunner { + private static final Logger logger = LoggerFactory.getLogger(QueryWrapper.class); + private static final MemoryMXBean memMXBean = ManagementFactory.getMemoryMXBean(); + + private final QueryWrapper query; + private final WorkManager workManager; + private final WebUserConnection webUserConnection; + private final SessionOptionManager options; -@XmlRootElement -public class QueryWrapper { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(QueryWrapper.class); - - private final String query; - private final String queryType; - private final int autoLimitRowCount; - private final String userName; - private final String defaultSchema; - - private static MemoryMXBean memMXBean = ManagementFactory.getMemoryMXBean(); - - @JsonCreator - public QueryWrapper( - @JsonProperty("query") String query, - @JsonProperty("queryType") String queryType, - @JsonProperty("autoLimit") String autoLimit, - @JsonProperty("userName") String userName, - @JsonProperty("defaultSchema") String defaultSchema) { + public RestQueryRunner(final QueryWrapper query, final WorkManager workManager, final WebUserConnection webUserConnection) { this.query = query; - this.queryType = queryType.toUpperCase(); - this.autoLimitRowCount = autoLimit != null && autoLimit.matches("[0-9]+") ? Integer.valueOf(autoLimit) : 0; - this.userName = userName; - this.defaultSchema = defaultSchema; + this.workManager = workManager; + this.webUserConnection = webUserConnection; + this.options = webUserConnection.getSession().getOptions(); } - public String getQuery() { - return query; + public RestQueryRunner.QueryResult run() throws Exception { + applyUserName(); + applyOptions(); + applyDefaultSchema(); + int maxRows = applyRowLimit(); + return submitQuery(maxRows); } - public String getQueryType() { - return queryType; + private void applyUserName() { + String userName = query.getUserName(); + if (!Strings.isNullOrEmpty(userName)) { + DrillConfig config = workManager.getContext().getConfig(); + if (!config.getBoolean(ExecConstants.IMPERSONATION_ENABLED)) { + throw UserException.permissionError() + .message("User impersonation is not enabled") + .build(logger); + } + InboundImpersonationManager inboundImpersonationManager = new InboundImpersonationManager(); + boolean isAdmin = !config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED) || + ImpersonationUtil.hasAdminPrivileges( + webUserConnection.getSession().getCredentials().getUserName(), + ExecConstants.ADMIN_USERS_VALIDATOR.getAdminUsers(options), + ExecConstants.ADMIN_USER_GROUPS_VALIDATOR.getAdminUserGroups(options)); + if (isAdmin) { + // Admin user can impersonate any user they want to (when authentication is disabled, all users are admin) + webUserConnection.getSession().replaceUserCredentials( + inboundImpersonationManager, + UserBitShared.UserCredentials.newBuilder().setUserName(userName).build()); + } else { + // Check configured impersonation rules to see if this user is allowed to impersonate the given user + inboundImpersonationManager.replaceUserOnSession(userName, webUserConnection.getSession()); + } + } } - public QueryType getType() { - return QueryType.valueOf(queryType); + private void applyOptions() { + Map<String, String> options = query.getOptions(); + if (options != null) { + SessionOptionManager sessionOptionManager = webUserConnection.getSession().getOptions(); + for (Map.Entry<String, String> entry : options.entrySet()) { + sessionOptionManager.setLocalOption(entry.getKey(), entry.getValue()); + } + } } - public QueryResult run(final WorkManager workManager, final WebUserConnection webUserConnection) throws Exception { - final RunQuery runQuery = RunQuery.newBuilder().setType(getType()) - .setPlan(getQuery()) - .setResultsMode(QueryResultsMode.STREAM_FULL) - .setAutolimitRowcount(autoLimitRowCount) - .build(); - - applyUserName(workManager, webUserConnection); - - int defaultMaxRows = webUserConnection.getSession().getOptions().getOption(ExecConstants.QUERY_MAX_ROWS).num_val.intValue(); + private void applyDefaultSchema() throws ValidationException { + String defaultSchema = query.getDefaultSchema(); if (!Strings.isNullOrEmpty(defaultSchema)) { SessionOptionManager options = webUserConnection.getSession().getOptions(); + @SuppressWarnings("resource") SchemaTreeProvider schemaTreeProvider = new SchemaTreeProvider(workManager.getContext()); SchemaPlus rootSchema = schemaTreeProvider.createRootSchema(options); webUserConnection.getSession().setDefaultSchemaPath(defaultSchema, rootSchema); } + } + private int applyRowLimit() { + int defaultMaxRows = webUserConnection.getSession().getOptions().getInt(ExecConstants.QUERY_MAX_ROWS); int maxRows; - if (autoLimitRowCount > 0 && defaultMaxRows > 0) { - maxRows = Math.min(autoLimitRowCount, defaultMaxRows); + int limit = query.getAutoLimitRowCount(); + if (limit > 0 && defaultMaxRows > 0) { + maxRows = Math.min(limit, defaultMaxRows); } else { - maxRows = Math.max(autoLimitRowCount, defaultMaxRows); + maxRows = Math.max(limit, defaultMaxRows); } webUserConnection.setAutoLimitRowCount(maxRows); + return maxRows; + } + + public RestQueryRunner.QueryResult submitQuery(int maxRows) { + final RunQuery runQuery = RunQuery.newBuilder() + .setType(QueryType.valueOf(query.getQueryType())) + .setPlan(query.getQuery()) + .setResultsMode(QueryResultsMode.STREAM_FULL) + .setAutolimitRowcount(maxRows) + .build(); // Heap usage threshold/trigger to provide resiliency on web server for queries submitted via HTTP double memoryFailureThreshold = workManager.getContext().getConfig().getDouble(ExecConstants.HTTP_MEMORY_HEAP_FAILURE_THRESHOLD); @@ -155,30 +185,6 @@ public class QueryWrapper { return new QueryResult(queryId, webUserConnection, webUserConnection.results); } - private void applyUserName(WorkManager workManager, WebUserConnection webUserConnection) { - SessionOptionManager options = webUserConnection.getSession().getOptions(); - DrillConfig config = workManager.getContext().getConfig(); - if (!Strings.isNullOrEmpty(userName)) { - if (!config.getBoolean(ExecConstants.IMPERSONATION_ENABLED)) { - throw UserException.permissionError().message("User impersonation is not enabled").build(logger); - } - InboundImpersonationManager inboundImpersonationManager = new InboundImpersonationManager(); - boolean isAdmin = !config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED) || - ImpersonationUtil.hasAdminPrivileges(webUserConnection.getSession().getCredentials().getUserName(), - ExecConstants.ADMIN_USERS_VALIDATOR.getAdminUsers(options), - ExecConstants.ADMIN_USER_GROUPS_VALIDATOR.getAdminUserGroups(options)); - if (isAdmin) { - // Admin user can impersonate any user they want to (when authentication is disabled, all users are admin) - webUserConnection.getSession().replaceUserCredentials( - inboundImpersonationManager, - UserBitShared.UserCredentials.newBuilder().setUserName(userName).build()); - } else { - // Check configured impersonation rules to see if this user is allowed to impersonate the given user - inboundImpersonationManager.replaceUserOnSession(userName, webUserConnection.getSession()); - } - } - } - //Detect possible excess heap private float getHeapUsage() { return (float) memMXBean.getHeapMemoryUsage().getUsed() / memMXBean.getHeapMemoryUsage().getMax(); @@ -206,10 +212,4 @@ public class QueryWrapper { return queryId; } } - - @Override - public String toString() { - return "QueryRequest [queryType=" + queryType + ", query=" + query + "]"; - } - -} +} \ No newline at end of file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java index 17d97d7..6819457 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java @@ -63,6 +63,8 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.glassfish.jersey.servlet.ServletContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.servlet.DispatcherType; import javax.servlet.http.HttpSession; @@ -88,6 +90,8 @@ import java.util.stream.Stream; * Wrapper class around jetty based web server. */ public class WebServer implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(WebServer.class); + private static final String ACE_MODE_SQL_TEMPLATE_JS = "ace.mode-sql.template.js"; private static final String ACE_MODE_SQL_JS = "mode-sql.js"; private static final String DRILL_FUNCTIONS_PLACEHOLDER = "__DRILL_FUNCTIONS__"; @@ -95,7 +99,6 @@ public class WebServer implements AutoCloseable { private static final String STATUS_THREADS_PATH = "/status/threads"; private static final String STATUS_METRICS_PATH = "/status/metrics"; - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WebServer.class); private static final String OPTIONS_DESCRIBE_JS = "options.describe.js"; private static final String OPTIONS_DESCRIBE_TEMPLATE_JS = "options.describe.template.js"; @@ -145,7 +148,6 @@ public class WebServer implements AutoCloseable { return; } - final QueuedThreadPool threadPool = new QueuedThreadPool(2, 2); embeddedJetty = new Server(threadPool); @@ -200,18 +202,20 @@ public class WebServer implements AutoCloseable { servletContextHandler.addServlet(new ServletHolder(new ThreadDumpServlet()), STATUS_THREADS_PATH); final ServletHolder staticHolder = new ServletHolder("static", DefaultServlet.class); + // Get resource URL for Drill static assets, based on where Drill icon is located String drillIconResourcePath = - Resource.newClassPathResource(BASE_STATIC_PATH + DRILL_ICON_RESOURCE_RELATIVE_PATH).getURL().toString(); + Resource.newClassPathResource(BASE_STATIC_PATH + DRILL_ICON_RESOURCE_RELATIVE_PATH).getURI().toString(); staticHolder.setInitParameter("resourceBase", drillIconResourcePath.substring(0, drillIconResourcePath.length() - DRILL_ICON_RESOURCE_RELATIVE_PATH.length())); staticHolder.setInitParameter("dirAllowed", "false"); staticHolder.setInitParameter("pathInfoOnly", "true"); servletContextHandler.addServlet(staticHolder, "/static/*"); - //Add Local path resource (This will allow access to dynamically created files like JavaScript) + // Add Local path resource (This will allow access to dynamically created files like JavaScript) final ServletHolder dynamicHolder = new ServletHolder("dynamic", DefaultServlet.class); - //Skip if unable to get a temp directory (e.g. during Unit tests) + + // Skip if unable to get a temp directory (e.g. during Unit tests) if (getOrCreateTmpJavaScriptDir() != null) { dynamicHolder.setInitParameter("resourceBase", getOrCreateTmpJavaScriptDir().getAbsolutePath()); dynamicHolder.setInitParameter("dirAllowed", "true"); @@ -220,7 +224,7 @@ public class WebServer implements AutoCloseable { } if (authEnabled) { - //DrillSecurityHandler is used to support SPNEGO and FORM authentication together + // DrillSecurityHandler is used to support SPNEGO and FORM authentication together servletContextHandler.setSecurityHandler(new DrillHttpSecurityHandlerProvider(config, workManager.getContext())); servletContextHandler.setSessionHandler(createSessionHandler(servletContextHandler.getSecurityHandler())); } @@ -247,10 +251,11 @@ public class WebServer implements AutoCloseable { } } - //Allow for Other Drillbits to make REST calls + // Allow for Other Drillbits to make REST calls FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class); filterHolder.setInitParameter("allowedOrigins", "*"); - //Allowing CORS for metrics only + + // Allowing CORS for metrics only servletContextHandler.addFilter(filterHolder, STATUS_METRICS_PATH, null); FilterHolder responseHeadersSettingFilter = new FilterHolder(ResponseHeadersSettingFilter.class); @@ -403,7 +408,6 @@ public class WebServer implements AutoCloseable { return tmpJavaScriptDir; } - /** * Generate Options Description JavaScript to serve http://drillhost/options ACE library search features * @throws IOException when unable to generate functions JS file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java index e14e696..1cdc359 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java @@ -58,13 +58,15 @@ import org.apache.drill.exec.store.sys.PersistentStoreProvider; import org.apache.drill.exec.work.WorkManager; import org.apache.drill.exec.work.foreman.Foreman; import org.glassfish.jersey.server.mvc.Viewable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.drill.shaded.guava.com.google.common.base.Joiner; import org.apache.drill.shaded.guava.com.google.common.collect.Lists; @Path("/") @RolesAllowed(DrillUserPrincipal.AUTHENTICATED_ROLE) public class ProfileResources { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ProfileResources.class); + private static final Logger logger = LoggerFactory.getLogger(ProfileResources.class); @Inject UserAuthEnabled authEnabled; @@ -208,9 +210,9 @@ public class ProfileResources { @XmlRootElement public class QProfiles { - private List<ProfileInfo> runningQueries; - private List<ProfileInfo> finishedQueries; - private List<String> errors; + private final List<ProfileInfo> runningQueries; + private final List<ProfileInfo> finishedQueries; + private final List<String> errors; public QProfiles(List<ProfileInfo> runningQueries, List<ProfileInfo> finishedQueries, List<String> errors) { this.runningQueries = runningQueries; @@ -368,7 +370,6 @@ public class ProfileResources { .build(logger); } - @GET @Path("/profiles/{queryid}.json") @Produces(MediaType.APPLICATION_JSON) @@ -440,4 +441,3 @@ public class ProfileResources { } } } - diff --git a/exec/java-exec/src/main/resources/rest/profile/profile.ftl b/exec/java-exec/src/main/resources/rest/profile/profile.ftl index 704c2dd..99b27b9 100644 --- a/exec/java-exec/src/main/resources/rest/profile/profile.ftl +++ b/exec/java-exec/src/main/resources/rest/profile/profile.ftl @@ -87,7 +87,7 @@ //Injects Estimated Rows function injectEstimatedRows() { Object.keys(opRowCountMap).forEach(key => { - var tgtElem = $("td.estRowsAnchor[key='" + key + "']"); + var tgtElem = $("td.estRowsAnchor[key='" + key + "']"); var status = tgtElem.append("<div class='estRows' title='Estimated'>(" + opRowCountMap[key] + ")</div>"); }); } @@ -164,18 +164,16 @@ <div id="query-edit" class="tab-pane"> <p> - <#if model.isOnlyImpersonationEnabled()> - <div class="form-group"> - <label for="userName">User Name</label> - <input type="text" size="30" name="userName" id="userName" placeholder="User Name" value="${model.getProfile().user}"> - </div> - </#if> - - <form role="form" id="queryForm" action="/query" method="POST"> - <div id="query-editor" class="form-group">${model.getProfile().query}</div> - <input class="form-control" id="query" name="query" type="hidden" value="${model.getProfile().query}"/> - <div style="padding:5px"><b>Hint: </b>Use <div id="keyboardHint" style="display:inline-block; font-style:italic"></div> to submit</div> + <#-- DRILL-7697: merge with copy in query.ftl --> + <form role="form" id="queryForm" action="/query" method="POST"> + <#if model.isOnlyImpersonationEnabled()> + <div class="form-group"> + <label for="userName">User Name</label> + <input type="text" size="30" name="userName" id="userName" placeholder="User Name" value="${model.getProfile().user}"> + </div> + </#if> <div class="form-group"> + <label for="queryType">Query type: </label> <div class="radio-inline"> <label> <input type="radio" name="queryType" id="sql" value="SQL" checked> @@ -185,22 +183,39 @@ <div class="radio-inline"> <label> <input type="radio" name="queryType" id="physical" value="PHYSICAL"> - PHYSICAL + Physical </label> </div> <div class="radio-inline"> <label> <input type="radio" name="queryType" id="logical" value="LOGICAL"> - LOGICAL + Logical </label> </div> + + <div class="form-group"> + <div style="display: inline-block"><label for="query">Query</label></div> + <div style="display: inline-block; float:right; padding-right:5%"><b>Hint: </b>Use + <div id="keyboardHint" style="display:inline-block; font-style:italic"></div> to submit</div> + <div id="query-editor">${model.getProfile().query}</div> + <input class="form-control" id="query" name="query" type="hidden" value="${model.getProfile().query}"/> </div> + <div> - <button class="btn btn-default" type="button" id="rerunButton" onclick="<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>"> - Re-run query + <button class="btn btn-primary" type="button" id="rerunButton" onclick="<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>"> + Re-run query </button> - <input type="checkbox" name="forceLimit" value="limit" <#if model.hasAutoLimit()>checked</#if>> Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="<#if model.hasAutoLimit()>${model.getAutoLimit()?c}<#else>${model.getDefaultAutoLimit()?c}</#if>" size="6" pattern="[0-9]*"> rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. Ignored if query has a limit already" style="cursor:pointer"></span> - </div> + + <input type="checkbox" name="forceLimit" value="limit" <#if model.hasAutoLimit()>checked</#if>> + Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" + value="<#if model.hasAutoLimit()>${model.getAutoLimit()?c}<#else>${model.getDefaultAutoLimit()?c}</#if>" size="6" pattern="[0-9]*"> + rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. + Ignored if query has a limit already" style="cursor:pointer"></span> + + Default schema: <input type="text" size="10" name="defaultSchema" id="defaultSchema"> + <span class="glyphicon glyphicon-info-sign" title="Set the default schema used to find table names + and for SHOW FILES and SHOW TABLES." style="cursor:pointer"></span> + </div> <input type="hidden" name="csrfToken" value="${model.getCsrfToken()}"> </form> </p> @@ -620,7 +635,7 @@ document.getElementById('queryForm') .addEventListener('keydown', function(e) { if (!(e.keyCode == 13 && (e.metaKey || e.ctrlKey))) return; - if (e.target.form) + if (e.target.form) <#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>; }); diff --git a/exec/java-exec/src/main/resources/rest/query/query.ftl b/exec/java-exec/src/main/resources/rest/query/query.ftl index 1034674..d124e95 100644 --- a/exec/java-exec/src/main/resources/rest/query/query.ftl +++ b/exec/java-exec/src/main/resources/rest/query/query.ftl @@ -43,6 +43,7 @@ <#include "*/runningQuery.ftl"> + <#-- DRILL-7697: merge with copy in profile.ftl --> <form role="form" id="queryForm" action="/query" method="POST"> <#if model.isOnlyImpersonationEnabled()> <div class="form-group"> @@ -51,49 +52,55 @@ </div> </#if> <div class="form-group"> - <label for="queryType">Query Type</label> - <div class="radio"> + <label for="queryType">Query type: </label> + <div class="radio-inline"> <label> <input type="radio" name="queryType" id="sql" value="SQL" checked> SQL </label> </div> - <div class="radio"> + <div class="radio-inline"> <label> <input type="radio" name="queryType" id="physical" value="PHYSICAL"> - PHYSICAL + Physical </label> </div> - <div class="radio"> + <div class="radio-inline"> <label> <input type="radio" name="queryType" id="logical" value="LOGICAL"> - LOGICAL + Logical </label> </div> </div> + <div class="form-group"> <div style="display: inline-block"><label for="query">Query</label></div> - <div style="display: inline-block; float:right; padding-right:5%"><b>Hint: </b>Use <div id="keyboardHint" style="display:inline-block; font-style:italic"></div> to submit</div> + <div style="display: inline-block; float:right; padding-right:5%"><b>Hint: </b>Use + <div id="keyboardHint" style="display:inline-block; font-style:italic"></div> to submit</div> <div id="query-editor-format"></div> <input class="form-control" type="hidden" id="query" name="query" autofocus/> </div> - <button class="btn btn-default" type="button" onclick="<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>"> + <button class="btn btn-primary" type="button" onclick="<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>"> Submit </button> - <input type="checkbox" name="forceLimit" value="limit" <#if model.isAutoLimitEnabled()>checked</#if>> Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="${model.getDefaultRowsAutoLimited()?c}" size="6" pattern="[0-9]*"> rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. Ignored if query has a limit already" style="cursor:pointer"></span> - <label for="defaultSchema"> - Default Schema - <input type="text" name="defaultSchema" id="defaultSchema" list="enabledPlugins" placeholder="-- default schema --"> - <datalist id="enabledPlugins"> - <#list model.getEnabledPlugins() as pluginModel> - <#if pluginModel.getPlugin()?? && pluginModel.getPlugin().enabled() == true> - <option value="${pluginModel.getPlugin().getName()}"> - </#if> - </#list> - </datalist> - </label> - <span class="glyphicon glyphicon-info-sign" title="Set the default schema used to find table names, and for SHOW FILES and SHOW TABLES" style="cursor:pointer"></span> + + <input type="checkbox" name="forceLimit" value="limit" <#if model.isAutoLimitEnabled()>checked</#if>> + Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="${model.getDefaultRowsAutoLimited()?c}" size="6" pattern="[0-9]*"> + rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. + Ignored if query has a LIMIT clause." style="cursor:pointer"></span> + + Default schema: + <input type="text" name="defaultSchema" id="defaultSchema" list="enabledPlugins" placeholder="schema"> + <datalist id="enabledPlugins"> + <#list model.getEnabledPlugins() as pluginModel> + <#if pluginModel.getPlugin()?? && pluginModel.getPlugin().enabled() == true> + <option value="${pluginModel.getPlugin().getName()}"> + </#if> + </#list> + </datalist> + <span class="glyphicon glyphicon-info-sign" title="Set the default schema used to find table names + and for SHOW FILES and SHOW TABLES." style="cursor:pointer"></span> <input type="hidden" name="csrfToken" value="${model.getCsrfToken()}"> </form> @@ -179,7 +186,7 @@ document.getElementById('queryForm') .addEventListener('keydown', function(e) { if (!(e.keyCode == 13 && (e.metaKey || e.ctrlKey))) return; - if (e.target.form) //Submit [Wrapped] Query + if (e.target.form) //Submit [Wrapped] Query <#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>; }); </script> diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/options/OptionValueTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/options/OptionValueTest.java index 56f4ba6..8c30723 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/server/options/OptionValueTest.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/options/OptionValueTest.java @@ -17,11 +17,15 @@ */ package org.apache.drill.exec.server.options; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.drill.common.exceptions.UserException; import org.apache.drill.test.BaseTest; -import org.junit.Assert; import org.junit.Test; public class OptionValueTest extends BaseTest { + @Test public void createBooleanKindTest() { final OptionValue createdValue = OptionValue.create( @@ -31,7 +35,7 @@ public class OptionValueTest extends BaseTest { final OptionValue expectedValue = OptionValue.create( OptionValue.AccessibleScopes.ALL, "myOption", true, OptionValue.OptionScope.SYSTEM); - Assert.assertEquals(expectedValue, createdValue); + assertEquals(expectedValue, createdValue); } @Test @@ -43,7 +47,16 @@ public class OptionValueTest extends BaseTest { final OptionValue expectedValue = OptionValue.create( OptionValue.AccessibleScopes.ALL, "myOption", 1.5, OptionValue.OptionScope.SYSTEM); - Assert.assertEquals(expectedValue, createdValue); + assertEquals(expectedValue, createdValue); + + try { + OptionValue.create( + OptionValue.Kind.DOUBLE, OptionValue.AccessibleScopes.ALL, + "myOption", "bogus", OptionValue.OptionScope.SYSTEM); + fail(); + } catch (UserException e) { + // Expected + } } @Test @@ -55,7 +68,16 @@ public class OptionValueTest extends BaseTest { final OptionValue expectedValue = OptionValue.create( OptionValue.AccessibleScopes.ALL, "myOption", 3000l, OptionValue.OptionScope.SYSTEM); - Assert.assertEquals(expectedValue, createdValue); + assertEquals(expectedValue, createdValue); + + try { + OptionValue.create( + OptionValue.Kind.LONG, OptionValue.AccessibleScopes.ALL, + "myOption", "bogus", OptionValue.OptionScope.SYSTEM); + fail(); + } catch (UserException e) { + // Expected + } } @Test @@ -67,6 +89,6 @@ public class OptionValueTest extends BaseTest { final OptionValue expectedValue = OptionValue.create( OptionValue.AccessibleScopes.ALL, "myOption", "wabalubawubdub", OptionValue.OptionScope.SYSTEM); - Assert.assertEquals(expectedValue, createdValue); + assertEquals(expectedValue, createdValue); } } diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/InteractiveUI.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/InteractiveUI.java new file mode 100644 index 0000000..1e2617b --- /dev/null +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/InteractiveUI.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.drill.exec.server.rest; + +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.test.ClusterFixtureBuilder; +import org.apache.drill.test.ClusterTest; + +/** + * Quick-and-dirty tool to run the Web UI for debugging without having + * to wait or a full build to run using {@code drillbit.sh}. + */ +public class InteractiveUI extends ClusterTest { + + public static void main(String[] args) { + ClusterFixtureBuilder builder = new ClusterFixtureBuilder(); + builder.configBuilder().put(ExecConstants.HTTP_ENABLE, true); + try { + startCluster(builder); + for (;;) { + Thread.sleep(1000); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/RestServerTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/RestServerTest.java index 2915d28..0e9e48d 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/RestServerTest.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/RestServerTest.java @@ -20,24 +20,32 @@ package org.apache.drill.exec.server.rest; import io.netty.channel.DefaultChannelPromise; import io.netty.channel.local.LocalAddress; import org.apache.drill.exec.proto.UserBitShared; +import org.apache.drill.exec.proto.UserBitShared.QueryProfile; import org.apache.drill.exec.proto.helper.QueryIdHelper; import org.apache.drill.exec.rpc.user.UserSession; import org.apache.drill.exec.server.options.SystemOptionManager; +import org.apache.drill.exec.server.rest.QueryWrapper.RestQueryBuilder; +import org.apache.drill.exec.server.rest.RestQueryRunner.QueryResult; import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal; import org.apache.drill.exec.work.WorkManager; import org.apache.drill.exec.work.foreman.Foreman; import org.apache.drill.test.ClusterTest; public class RestServerTest extends ClusterTest { - protected QueryWrapper.QueryResult runQuery(String sql) throws Exception { - return runQuery(new QueryWrapper(sql, "SQL", null, null, null)); + + protected QueryResult runQuery(String sql) throws Exception { + return runQuery(new RestQueryBuilder().query(sql).build()); } - protected QueryWrapper.QueryResult runQueryWithUsername(String sql, String userName) throws Exception { - return runQuery(new QueryWrapper(sql, "SQL", null, userName, null)); + protected QueryResult runQueryWithUsername(String sql, String userName) throws Exception { + return runQuery( + new RestQueryBuilder() + .query(sql) + .userName(userName) + .build()); } - protected QueryWrapper.QueryResult runQuery(QueryWrapper q) throws Exception { + protected QueryResult runQuery(QueryWrapper q) throws Exception { SystemOptionManager systemOptions = cluster.drillbit().getContext().getOptionManager(); DrillUserPrincipal principal = new DrillUserPrincipal.AnonDrillUserPrincipal(); WebSessionResources webSessionResources = new WebSessionResources( @@ -49,11 +57,10 @@ public class RestServerTest extends ClusterTest { .build(), new DefaultChannelPromise(null)); WebUserConnection connection = new WebUserConnection.AnonWebUserConnection(webSessionResources); - return q.run(cluster.drillbit().getManager(), connection); + return new RestQueryRunner(q, cluster.drillbit().getManager(), connection).run(); } - - protected UserBitShared.QueryProfile getQueryProfile(QueryWrapper.QueryResult result) { + protected QueryProfile getQueryProfile(QueryResult result) { String queryId = result.getQueryId(); WorkManager workManager = cluster.drillbit().getManager(); Foreman f = workManager.getBee().getForemanForQueryId(QueryIdHelper.getQueryIdFromString(queryId)); @@ -65,5 +72,4 @@ public class RestServerTest extends ClusterTest { } return workManager.getContext().getProfileStoreContext().getCompletedProfileStore().get(queryId); } - } diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapper.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapper.java index aaa7931..4c13310 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapper.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapper.java @@ -17,18 +17,23 @@ */ package org.apache.drill.exec.server.rest; -import org.apache.drill.common.exceptions.UserException; -import org.apache.drill.exec.ExecConstants; -import org.apache.drill.test.ClusterFixture; -import org.junit.BeforeClass; -import org.junit.Test; - import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import java.util.HashMap; +import java.util.Map; + +import org.apache.drill.common.exceptions.UserException; +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.exec.server.rest.QueryWrapper.RestQueryBuilder; +import org.apache.drill.exec.server.rest.RestQueryRunner.QueryResult; +import org.apache.drill.test.ClusterFixture; +import org.junit.BeforeClass; +import org.junit.Test; + public class TestQueryWrapper extends RestServerTest { @BeforeClass @@ -40,7 +45,7 @@ public class TestQueryWrapper extends RestServerTest { @Test public void testShowSchemas() throws Exception { - QueryWrapper.QueryResult result = runQuery("SHOW SCHEMAS"); + QueryResult result = runQuery("SHOW SCHEMAS"); assertEquals("COMPLETED", result.queryState); assertNotEquals(0, result.rows.size()); assertEquals(1, result.columns.size()); @@ -50,8 +55,10 @@ public class TestQueryWrapper extends RestServerTest { @Test public void testImpersonationDisabled() throws Exception { try { - QueryWrapper q = new QueryWrapper("SHOW SCHEMAS", "SQL", null, "alfred", null); - runQuery(q); + runQuery(new RestQueryBuilder() + .query("SHOW SCHEMAS") + .userName("alfred") + .build()); fail("Should have thrown exception"); } catch (UserException e) { assertThat(e.getMessage(), containsString("User impersonation is not enabled")); @@ -60,9 +67,83 @@ public class TestQueryWrapper extends RestServerTest { @Test public void testSpecifyDefaultSchema() throws Exception { - QueryWrapper.QueryResult result = runQuery(new QueryWrapper("SHOW FILES", "SQL", null, null, "dfs.tmp")); + QueryResult result = runQuery( + new RestQueryBuilder() + .query("SHOW FILES") + .defaultSchema("dfs.tmp") + .build()); // SHOW FILES will fail if default schema is not provided assertEquals("COMPLETED", result.queryState); } + protected QueryResult runQueryWithOption(String sql, String name, String value) throws Exception { + Map<String, String> options = new HashMap<>(); + options.put(name, value); + return runQuery( + new RestQueryBuilder() + .query(sql) + .sessionOptions(options) + .build()); + } + + @Test + public void testOptionWithQuery() throws Exception { + runOptionTest(ExecConstants.ENABLE_VERBOSE_ERRORS_KEY, "true"); // Boolean + runOptionTest(ExecConstants.QUERY_MAX_ROWS, "10"); // Long + runOptionTest(ExecConstants.TEXT_ESTIMATED_ROW_SIZE_KEY, "10.5"); // Double + runOptionTest(ExecConstants.OUTPUT_FORMAT_OPTION, "json"); // String + } + + public void runOptionTest(String option, String value) throws Exception { + String query = String.format("SELECT val FROM sys.options WHERE `name`='%s'", option); + String origValue = client.queryBuilder() + .sql(query) + .singletonString(); + assertNotEquals(origValue, value, + "Not a valid test: new value is the same as the current value"); + QueryResult result = runQueryWithOption(query, option, value); + assertEquals(1, result.rows.size()); + assertEquals(1, result.columns.size()); + assertEquals(value, result.rows.get(0).get("val")); + } + + @Test + public void testInvalidOptionName() throws Exception { + try { + runQueryWithOption("SHOW SCHEMAS", "xxx", "s"); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("The option 'xxx' does not exist.")); + } + } + + @Test + public void testInvalidBooleanOption() { + try { + runQueryWithOption("SHOW SCHEMAS", ExecConstants.ENABLE_VERBOSE_ERRORS_KEY, "not a boolean"); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("not a valid value")); + } + } + + @Test + public void testInvalidLongOption() { + try { + runQueryWithOption("SHOW SCHEMAS", ExecConstants.QUERY_MAX_ROWS, "bogus"); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("not a valid value")); + } + } + + @Test + public void testInvalidDoubleOption() { + try { + runQueryWithOption("SHOW SCHEMAS", ExecConstants.TEXT_ESTIMATED_ROW_SIZE_KEY, "bogus"); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("not a valid value")); + } + } } diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapperImpersonation.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapperImpersonation.java index fef8f1a..188a3fe 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapperImpersonation.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/rest/TestQueryWrapperImpersonation.java @@ -19,6 +19,7 @@ package org.apache.drill.exec.server.rest; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.proto.UserBitShared; +import org.apache.drill.exec.server.rest.RestQueryRunner.QueryResult; import org.apache.drill.test.ClusterFixture; import org.junit.BeforeClass; import org.junit.Test; @@ -27,6 +28,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public final class TestQueryWrapperImpersonation extends RestServerTest { + @BeforeClass public static void setupServer() throws Exception { startCluster(ClusterFixture.bareBuilder(dirTestWatcher) @@ -36,7 +38,8 @@ public final class TestQueryWrapperImpersonation extends RestServerTest { @Test public void testImpersonation() throws Exception { - QueryWrapper.QueryResult result = runQueryWithUsername("SELECT CATALOG_NAME, SCHEMA_NAME FROM information_schema.SCHEMATA", "alfred"); + QueryResult result = runQueryWithUsername( + "SELECT CATALOG_NAME, SCHEMA_NAME FROM information_schema.SCHEMATA", "alfred"); UserBitShared.QueryProfile queryProfile = getQueryProfile(result); assertNotNull(queryProfile); assertEquals("alfred", queryProfile.getUser()); @@ -44,10 +47,10 @@ public final class TestQueryWrapperImpersonation extends RestServerTest { @Test public void testImpersonationEnabledButUserNameNotProvided() throws Exception { - QueryWrapper.QueryResult result = runQueryWithUsername("SELECT CATALOG_NAME, SCHEMA_NAME FROM information_schema.SCHEMATA", null); + QueryResult result = runQueryWithUsername( + "SELECT CATALOG_NAME, SCHEMA_NAME FROM information_schema.SCHEMATA", null); UserBitShared.QueryProfile queryProfile = getQueryProfile(result); assertNotNull(queryProfile); assertEquals("anonymous", queryProfile.getUser()); } - } diff --git a/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixture.java b/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixture.java index c8b64a4..1ba053c 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixture.java +++ b/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixture.java @@ -248,6 +248,11 @@ public class ClusterFixture extends BaseFixture implements AutoCloseable { } private void configureStoragePlugins(Drillbit bit) throws Exception { + + // Skip plugins if not running in test mode. + if (builder.dirTestWatcher == null) { + return; + } // Create the dfs name space builder.dirTestWatcher.newDfsTestTmpDir(); diff --git a/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixtureBuilder.java b/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixtureBuilder.java index 9bc5312..5aa9ff6 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixtureBuilder.java +++ b/exec/java-exec/src/test/java/org/apache/drill/test/ClusterFixtureBuilder.java @@ -30,6 +30,15 @@ import org.apache.drill.shaded.guava.com.google.common.base.Preconditions; * Build a Drillbit and client with the options provided. The simplest * builder starts an embedded Drillbit, with the "dfs" name space, * a max width (parallelization) of 2. + * <p> + * Designed primarily for unit tests: the builders provide control + * over all aspects of the Drillbit or cluster. Can also be used to + * create an embedded Drillbit, use the zero-argument + * constructor which will omit creating set of test-only directories + * and will skip creating the test-only storage plugins and other + * configuration. In this mode, you should configure the builder + * to read from a config file, or specify all the non-default + * config options needed. */ public class ClusterFixtureBuilder { @@ -58,6 +67,10 @@ public class ClusterFixtureBuilder { protected Properties clientProps; protected final BaseDirTestWatcher dirTestWatcher; + public ClusterFixtureBuilder() { + this.dirTestWatcher = null; + } + public ClusterFixtureBuilder(BaseDirTestWatcher dirTestWatcher) { this.dirTestWatcher = Preconditions.checkNotNull(dirTestWatcher); }