blob: cbaa300d52966dd7604aab4c76bd030b00d590aa [file] [log] [blame]
/*
* 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.druid.sql.calcite.planner;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import org.apache.calcite.DataContext;
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.avatica.remote.TypedValue;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Numbers;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.QueryContext;
import org.apache.druid.query.QueryContexts;
import org.apache.druid.query.lookup.LookupExtractor;
import org.apache.druid.query.lookup.LookupExtractorFactoryContainerProvider;
import org.apache.druid.query.lookup.RegisteredLookupExtractionFn;
import org.apache.druid.segment.join.JoinableFactoryWrapper;
import org.apache.druid.server.security.Access;
import org.apache.druid.server.security.AuthenticationResult;
import org.apache.druid.server.security.ResourceAction;
import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.QueryLookupOperatorConversion;
import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry;
import org.apache.druid.sql.calcite.rule.AggregatePullUpLookupRule;
import org.apache.druid.sql.calcite.rule.ReverseLookupRule;
import org.apache.druid.sql.calcite.run.EngineFeature;
import org.apache.druid.sql.calcite.run.QueryMaker;
import org.apache.druid.sql.calcite.run.SqlEngine;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Like {@link PlannerConfig}, but that has static configuration and this class
* contains dynamic, per-query configuration. Additional Druid-specific static
* configuration resides in the {@link PlannerToolbox} class.
*/
public class PlannerContext
{
// Query context keys
public static final String CTX_SQL_CURRENT_TIMESTAMP = "sqlCurrentTimestamp";
public static final String CTX_SQL_TIME_ZONE = "sqlTimeZone";
public static final String CTX_SQL_JOIN_ALGORITHM = "sqlJoinAlgorithm";
private static final JoinAlgorithm DEFAULT_SQL_JOIN_ALGORITHM = JoinAlgorithm.BROADCAST;
/**
* Undocumented context key, used internally, to allow the web console to
* apply a limit without having to rewrite the SQL query.
*/
public static final String CTX_SQL_OUTER_LIMIT = "sqlOuterLimit";
/**
* Key to enable window functions.
*/
public static final String CTX_ENABLE_WINDOW_FNS = "enableWindowing";
/**
* Context key for {@link PlannerContext#isUseBoundsAndSelectors()}.
*/
public static final String CTX_SQL_USE_BOUNDS_AND_SELECTORS = "sqlUseBoundAndSelectors";
public static final boolean DEFAULT_SQL_USE_BOUNDS_AND_SELECTORS = NullHandling.replaceWithDefault();
/**
* Context key for {@link PlannerContext#isPullUpLookup()}.
*/
public static final String CTX_SQL_PULL_UP_LOOKUP = "sqlPullUpLookup";
public static final boolean DEFAULT_SQL_PULL_UP_LOOKUP = true;
/**
* Context key for {@link PlannerContext#isReverseLookup()}.
*/
public static final String CTX_SQL_REVERSE_LOOKUP = "sqlReverseLookup";
public static final boolean DEFAULT_SQL_REVERSE_LOOKUP = true;
// DataContext keys
public static final String DATA_CTX_AUTHENTICATION_RESULT = "authenticationResult";
private final PlannerToolbox plannerToolbox;
private final ExpressionParser expressionParser;
private final String sql;
private final PlannerConfig plannerConfig;
private final DateTime localNow;
private final SqlEngine engine;
private final Map<String, Object> queryContext;
private final String sqlQueryId;
private final boolean stringifyArrays;
private final boolean useBoundsAndSelectors;
private final boolean pullUpLookup;
private final boolean reverseLookup;
private final CopyOnWriteArrayList<String> nativeQueryIds = new CopyOnWriteArrayList<>();
private final PlannerHook hook;
// bindings for dynamic parameters to bind during planning
private List<TypedValue> parameters = Collections.emptyList();
// result of authentication, providing identity to authorize set of resources produced by validation
private AuthenticationResult authenticationResult;
// set of datasources and views which must be authorized, initialized to null so we can detect if it has been set.
private Set<ResourceAction> resourceActions;
// result of authorizing set of resources against authentication identity
private Access authorizationResult;
// error messages encountered while planning the query
@Nullable
private String planningError;
private QueryMaker queryMaker;
private VirtualColumnRegistry joinExpressionVirtualColumnRegistry;
// set of attributes for a SQL statement used in the EXPLAIN PLAN output
private ExplainAttributes explainAttributes;
private PlannerLookupCache lookupCache;
private PlannerContext(
final PlannerToolbox plannerToolbox,
final String sql,
final PlannerConfig plannerConfig,
final DateTime localNow,
final boolean stringifyArrays,
final boolean useBoundsAndSelectors,
final boolean pullUpLookup,
final boolean reverseLookup,
final SqlEngine engine,
final Map<String, Object> queryContext,
final PlannerHook hook
)
{
this.plannerToolbox = plannerToolbox;
this.expressionParser = new ExpressionParserImpl(plannerToolbox.exprMacroTable());
this.sql = sql;
this.plannerConfig = Preconditions.checkNotNull(plannerConfig, "plannerConfig");
this.engine = engine;
this.queryContext = queryContext;
this.localNow = Preconditions.checkNotNull(localNow, "localNow");
this.stringifyArrays = stringifyArrays;
this.useBoundsAndSelectors = useBoundsAndSelectors;
this.pullUpLookup = pullUpLookup;
this.reverseLookup = reverseLookup;
this.hook = hook == null ? NoOpPlannerHook.INSTANCE : hook;
String sqlQueryId = (String) this.queryContext.get(QueryContexts.CTX_SQL_QUERY_ID);
// special handling for DruidViewMacro, normal client will allocate sqlid in SqlLifecyle
if (Strings.isNullOrEmpty(sqlQueryId)) {
sqlQueryId = UUID.randomUUID().toString();
}
this.sqlQueryId = sqlQueryId;
}
public static PlannerContext create(
final PlannerToolbox plannerToolbox,
final String sql,
final SqlEngine engine,
final Map<String, Object> queryContext,
final PlannerHook hook
)
{
final DateTime utcNow;
final DateTimeZone timeZone;
final boolean stringifyArrays;
final boolean useBoundsAndSelectors;
final boolean pullUpLookup;
final boolean reverseLookup;
final Object stringifyParam = queryContext.get(QueryContexts.CTX_SQL_STRINGIFY_ARRAYS);
final Object tsParam = queryContext.get(CTX_SQL_CURRENT_TIMESTAMP);
final Object tzParam = queryContext.get(CTX_SQL_TIME_ZONE);
final Object useBoundsAndSelectorsParam = queryContext.get(CTX_SQL_USE_BOUNDS_AND_SELECTORS);
final Object pullUpLookupParam = queryContext.get(CTX_SQL_PULL_UP_LOOKUP);
final Object reverseLookupParam = queryContext.get(CTX_SQL_REVERSE_LOOKUP);
if (tsParam != null) {
utcNow = new DateTime(tsParam, DateTimeZone.UTC);
} else {
utcNow = new DateTime(DateTimeZone.UTC);
}
if (tzParam != null) {
timeZone = DateTimes.inferTzFromString(String.valueOf(tzParam));
} else {
timeZone = plannerToolbox.plannerConfig().getSqlTimeZone();
}
if (stringifyParam != null) {
stringifyArrays = Numbers.parseBoolean(stringifyParam);
} else {
stringifyArrays = true;
}
if (useBoundsAndSelectorsParam != null) {
useBoundsAndSelectors = Numbers.parseBoolean(useBoundsAndSelectorsParam);
} else {
useBoundsAndSelectors = DEFAULT_SQL_USE_BOUNDS_AND_SELECTORS;
}
if (pullUpLookupParam != null) {
pullUpLookup = Numbers.parseBoolean(pullUpLookupParam);
} else {
pullUpLookup = DEFAULT_SQL_PULL_UP_LOOKUP;
}
if (reverseLookupParam != null) {
reverseLookup = Numbers.parseBoolean(reverseLookupParam);
} else {
reverseLookup = DEFAULT_SQL_REVERSE_LOOKUP;
}
return new PlannerContext(
plannerToolbox,
sql,
plannerToolbox.plannerConfig().withOverrides(queryContext),
utcNow.withZone(timeZone),
stringifyArrays,
useBoundsAndSelectors,
pullUpLookup,
reverseLookup,
engine,
queryContext,
hook
);
}
/**
* Returns the join algorithm specified in a query context.
*/
public static JoinAlgorithm getJoinAlgorithm(QueryContext queryContext)
{
return getJoinAlgorithmFromContextValue(queryContext.get(CTX_SQL_JOIN_ALGORITHM));
}
/**
* Returns the join algorithm specified in a query context.
*/
public static JoinAlgorithm getJoinAlgorithm(Map<String, Object> queryContext)
{
return getJoinAlgorithmFromContextValue(queryContext.get(CTX_SQL_JOIN_ALGORITHM));
}
private static JoinAlgorithm getJoinAlgorithmFromContextValue(final Object object)
{
final String s = QueryContexts.getAsString(
CTX_SQL_JOIN_ALGORITHM,
object,
DEFAULT_SQL_JOIN_ALGORITHM.toString()
);
try {
return JoinAlgorithm.fromString(s);
}
catch (IllegalArgumentException e) {
throw QueryContexts.badValueException(
CTX_SQL_JOIN_ALGORITHM,
StringUtils.format("one of %s", Arrays.toString(JoinAlgorithm.values())),
object
);
}
}
public PlannerToolbox getPlannerToolbox()
{
return plannerToolbox;
}
public ExprMacroTable getExprMacroTable()
{
return plannerToolbox.exprMacroTable();
}
public ExpressionParser getExpressionParser()
{
return expressionParser;
}
/**
* Equivalent to {@link ExpressionParser#parse(String)} on {@link #getExpressionParser()}.
*/
public Expr parseExpression(final String expr)
{
return expressionParser.parse(expr);
}
// Deprecated: prefer using the toolbox
public ObjectMapper getJsonMapper()
{
return plannerToolbox.jsonMapper();
}
public PlannerConfig getPlannerConfig()
{
return plannerConfig;
}
public DateTime getLocalNow()
{
return localNow;
}
public DateTimeZone getTimeZone()
{
return localNow.getZone();
}
public JoinableFactoryWrapper getJoinableFactoryWrapper()
{
return plannerToolbox.joinableFactoryWrapper();
}
@Nullable
public String getSchemaResourceType(String schema, String resourceName)
{
return plannerToolbox.rootSchema().getResourceType(schema, resourceName);
}
/**
* Return the query context as a mutable map. Use this form when
* modifying the context during planning.
*/
public Map<String, Object> queryContextMap()
{
return queryContext;
}
/**
* Return the query context as an immutable object. Use this form
* when querying the context as it provides type-safe accessors.
*/
public QueryContext queryContext()
{
return QueryContext.of(queryContext);
}
public boolean isStringifyArrays()
{
return stringifyArrays;
}
/**
* Whether we should use {@link org.apache.druid.query.filter.BoundDimFilter} and
* {@link org.apache.druid.query.filter.SelectorDimFilter} (true) or {@link org.apache.druid.query.filter.RangeFilter},
* {@link org.apache.druid.query.filter.EqualityFilter}, and {@link org.apache.druid.query.filter.NullFilter} (false).
*
* Typically true when {@link NullHandling#replaceWithDefault()} and false when {@link NullHandling#sqlCompatible()}.
* Can be overriden by the undocumented context parameter {@link #CTX_SQL_USE_BOUNDS_AND_SELECTORS}.
*/
public boolean isUseBoundsAndSelectors()
{
return useBoundsAndSelectors;
}
/**
* Whether we should use {@link AggregatePullUpLookupRule} to pull LOOKUP functions on injective lookups up above
* a GROUP BY.
*/
public boolean isPullUpLookup()
{
return pullUpLookup;
}
/**
* Whether we should use {@link ReverseLookupRule} to reduce the LOOKUP function, and whether we should set the
* "optimize" flag on {@link RegisteredLookupExtractionFn}.
*/
public boolean isReverseLookup()
{
return reverseLookup;
}
public List<TypedValue> getParameters()
{
return parameters;
}
public AuthenticationResult getAuthenticationResult()
{
return Preconditions.checkNotNull(authenticationResult, "Authentication result not available");
}
public JoinAlgorithm getJoinAlgorithm()
{
return getJoinAlgorithm(queryContext);
}
public String getSql()
{
return sql;
}
public PlannerHook getPlannerHook()
{
return hook;
}
public String getSqlQueryId()
{
return sqlQueryId;
}
public CopyOnWriteArrayList<String> getNativeQueryIds()
{
return nativeQueryIds;
}
public void addNativeQueryId(String queryId)
{
this.nativeQueryIds.add(queryId);
}
@Nullable
public String getPlanningError()
{
return planningError;
}
/**
* Sets the planning error in the context that will be shown to the user if the SQL query cannot be translated
* to a native query. This error is often a hint and thus should be phrased as such. Also, the final plan can
* be very different from SQL that user has written. So again, the error should be phrased to indicate this gap
* clearly.
*/
public void setPlanningError(String formatText, Object... arguments)
{
planningError = StringUtils.nonStrictFormat(formatText, arguments);
}
public DataContext createDataContext(final JavaTypeFactory typeFactory, List<TypedValue> parameters)
{
class DruidDataContext implements DataContext
{
private final Map<String, Object> base_context = ImmutableMap.of(
DataContext.Variable.UTC_TIMESTAMP.camelName, localNow.getMillis(),
DataContext.Variable.CURRENT_TIMESTAMP.camelName, localNow.getMillis(),
DataContext.Variable.LOCAL_TIMESTAMP.camelName, new Interval(
new DateTime("1970-01-01T00:00:00.000", localNow.getZone()),
localNow
).toDurationMillis(),
DataContext.Variable.TIME_ZONE.camelName, localNow.getZone().toTimeZone().clone()
);
private final Map<String, Object> context;
DruidDataContext()
{
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
builder.putAll(base_context);
int i = 0;
for (TypedValue parameter : parameters) {
builder.put("?" + i, parameter.value);
i++;
}
if (authenticationResult != null) {
builder.put(DATA_CTX_AUTHENTICATION_RESULT, authenticationResult);
}
context = builder.build();
}
@Override
public SchemaPlus getRootSchema()
{
throw new UnsupportedOperationException();
}
@Override
public JavaTypeFactory getTypeFactory()
{
return typeFactory;
}
@Override
public QueryProvider getQueryProvider()
{
throw new UnsupportedOperationException();
}
@Override
public Object get(final String name)
{
return context.get(name);
}
}
return new DruidDataContext();
}
public Access getAuthorizationResult()
{
return Preconditions.checkNotNull(authorizationResult, "Authorization result not available");
}
public void setParameters(List<TypedValue> parameters)
{
this.parameters = Preconditions.checkNotNull(parameters, "parameters");
}
public void setAuthenticationResult(AuthenticationResult authenticationResult)
{
if (this.authenticationResult != null) {
// It's a bug if this happens, because setAuthenticationResult should be called exactly once.
throw new ISE("Authentication result has already been set");
}
this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult");
}
public void setAuthorizationResult(Access access)
{
if (this.authorizationResult != null) {
// It's a bug if this happens, because setAuthorizationResult should be called exactly once.
throw new ISE("Authorization result has already been set");
}
this.authorizationResult = Preconditions.checkNotNull(access, "authorizationResult");
}
public Set<ResourceAction> getResourceActions()
{
return Preconditions.checkNotNull(resourceActions, "Resources not available");
}
public void setResourceActions(Set<ResourceAction> resourceActions)
{
if (this.resourceActions != null) {
// It's a bug if this happens, because setResourceActions should be called exactly once.
throw new ISE("Resources have already been set");
}
this.resourceActions = Preconditions.checkNotNull(resourceActions, "resourceActions");
}
public void setQueryMaker(QueryMaker queryMaker)
{
if (this.queryMaker != null) {
// It's a bug if this happens, because setQueryMaker should be called exactly once.
throw new ISE("QueryMaker has already been set");
}
this.queryMaker = Preconditions.checkNotNull(queryMaker, "queryMaker");
}
public SqlEngine getEngine()
{
return engine;
}
/**
* Checks if the current {@link SqlEngine} supports a particular feature.
*
* When executing a specific query, use this method instead of {@link SqlEngine#featureAvailable(EngineFeature)}
* because it also verifies feature flags such as {@link #CTX_ENABLE_WINDOW_FNS}.
*/
public boolean featureAvailable(final EngineFeature feature)
{
if (feature == EngineFeature.WINDOW_FUNCTIONS &&
!QueryContexts.getAsBoolean(CTX_ENABLE_WINDOW_FNS, queryContext.get(CTX_ENABLE_WINDOW_FNS), false)) {
// Short-circuit: feature requires context flag.
return false;
}
if (feature == EngineFeature.TIME_BOUNDARY_QUERY && !queryContext().isTimeBoundaryPlanningEnabled()) {
// Short-circuit: feature requires context flag.
return false;
}
return engine.featureAvailable(feature);
}
public QueryMaker getQueryMaker()
{
return Preconditions.checkNotNull(queryMaker, "QueryMaker not available");
}
public VirtualColumnRegistry getJoinExpressionVirtualColumnRegistry()
{
return joinExpressionVirtualColumnRegistry;
}
public void setJoinExpressionVirtualColumnRegistry(VirtualColumnRegistry joinExpressionVirtualColumnRegistry)
{
this.joinExpressionVirtualColumnRegistry = joinExpressionVirtualColumnRegistry;
}
public ExplainAttributes getExplainAttributes()
{
return this.explainAttributes;
}
public void setExplainAttributes(ExplainAttributes explainAttributes)
{
if (this.explainAttributes != null) {
throw new ISE("ExplainAttributes has already been set");
}
this.explainAttributes = explainAttributes;
}
/**
* Retrieve a named {@link LookupExtractor}.
*/
public LookupExtractor getLookup(final String lookupName)
{
if (lookupCache == null) {
final SqlOperatorConversion lookupOperatorConversion =
plannerToolbox.operatorTable().lookupOperatorConversion(QueryLookupOperatorConversion.SQL_FUNCTION);
if (lookupOperatorConversion != null) {
final LookupExtractorFactoryContainerProvider lookupProvider =
((QueryLookupOperatorConversion) lookupOperatorConversion).getLookupExtractorFactoryContainerProvider();
lookupCache = new PlannerLookupCache(lookupProvider);
} else {
lookupCache = new PlannerLookupCache(null);
}
}
return lookupCache.getLookup(lookupName);
}
}