Repository navigation
DRILL-8555: Physical plan cache for parameterized SQL queries #3086
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a4c43b3
4136460
b003715
f5e64d5
8f1249a
3abcce8
d7c9ba6
8ff6bb6
53306c1
39d86b1
e16de1e
9ce7361
419403d
6aa9618
6ee1c59
b40a70d
1ae6773
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |
| package org.apache.drill.exec.store.iceberg.plan; | ||
|
|
||
| import org.apache.drill.common.FunctionNames; | ||
| import org.apache.drill.common.expression.BooleanOperator; | ||
| import org.apache.drill.common.expression.CastExpression; | ||
| import org.apache.drill.common.expression.FunctionCall; | ||
| import org.apache.drill.common.expression.LogicalExpression; | ||
| import org.apache.drill.common.expression.SchemaPath; | ||
|
|
@@ -32,6 +34,46 @@ public class DrillExprToIcebergTranslator extends AbstractExprVisitor<Expression | |
|
|
||
| public static final ExprVisitor<Expression, Void, RuntimeException> INSTANCE = new DrillExprToIcebergTranslator(); | ||
|
|
||
| @Override | ||
| public Expression visitBooleanOperator(BooleanOperator op, Void value) { | ||
| Expression result = null; | ||
| for (LogicalExpression argument : op.args()) { | ||
| Expression next = argument.accept(this, value); | ||
| if (next == null) { | ||
| return null; | ||
| } | ||
| result = result == null ? next : FunctionNames.AND.equals(op.getName()) | ||
| ? Expressions.and(result, next) : Expressions.or(result, next); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| @Override | ||
| public Expression visitCastExpression(CastExpression cast, Void value) { | ||
| Expression input = cast.getInput().accept(this, value); | ||
| if (!(input instanceof ConstantExpression)) { | ||
| return null; | ||
| } | ||
| Object literal = ((ConstantExpression<?>) input).getValue(); | ||
| if (!(literal instanceof Integer) && !(literal instanceof Long)) { | ||
| return null; | ||
| } | ||
| long millis = ((Number) literal).longValue(); | ||
| // Drill serializes temporal constants as casts of epoch milliseconds. | ||
| // Iceberg's DATE uses days; TIME and TIMESTAMP use microseconds. | ||
| switch (cast.getMajorType().getMinorType()) { | ||
| case BIGINT: | ||
| return new ConstantExpression<>(millis); | ||
| case DATE: | ||
| return new ConstantExpression<>(Math.toIntExact(Math.floorDiv(millis, 86_400_000L))); | ||
| case TIME: | ||
| case TIMESTAMP: | ||
| return new ConstantExpression<>(Math.multiplyExact(millis, 1_000L)); | ||
| default: | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public Expression visitFunctionCall(FunctionCall call, Void value) throws RuntimeException { | ||
| switch (call.getName()) { | ||
|
|
@@ -52,10 +94,8 @@ public Expression visitFunctionCall(FunctionCall call, Void value) throws Runtim | |
| return null; | ||
| } | ||
| case FunctionNames.NOT: { | ||
| Expression expression = call.arg(0).accept(this, null); | ||
| if (expression != null) { | ||
| return Expressions.not(expression); | ||
| } | ||
| // Complementing a two-valued Iceberg predicate does not preserve SQL | ||
| // UNKNOWN for null operands. Leave unsimplified NOT predicates in Drill. | ||
| return null; | ||
| } | ||
| case FunctionNames.IS_NULL: { | ||
|
|
@@ -79,7 +119,7 @@ public Expression visitFunctionCall(FunctionCall call, Void value) throws Runtim | |
| Expression expression = call.arg(1).accept(this, null); | ||
| if (nameRef instanceof SchemaPath && expression instanceof ConstantExpression) { | ||
| String name = IcebergGroupScan.getPath((SchemaPath) nameRef); | ||
| return Expressions.lessThan(name, ((ConstantExpression<?>) expression).getValue()); | ||
| return rejectNull(name, Expressions.lessThan(name, ((ConstantExpression<?>) expression).getValue())); | ||
| } | ||
| return null; | ||
| } | ||
|
|
@@ -88,7 +128,7 @@ public Expression visitFunctionCall(FunctionCall call, Void value) throws Runtim | |
| Expression expression = call.arg(1).accept(this, null); | ||
| if (nameRef instanceof SchemaPath && expression instanceof ConstantExpression) { | ||
| String name = IcebergGroupScan.getPath((SchemaPath) nameRef); | ||
| return Expressions.lessThanOrEqual(name, ((ConstantExpression<?>) expression).getValue()); | ||
| return rejectNull(name, Expressions.lessThanOrEqual(name, ((ConstantExpression<?>) expression).getValue())); | ||
| } | ||
| return null; | ||
| } | ||
|
|
@@ -103,7 +143,7 @@ public Expression visitFunctionCall(FunctionCall call, Void value) throws Runtim | |
| } | ||
| case FunctionNames.GE: { | ||
| LogicalExpression nameRef = call.args().get(0); | ||
| Expression expression = call.args().get(0).accept(this, null); | ||
| Expression expression = call.args().get(1).accept(this, null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Behaviour change for all queries] This changes Iceberg pushdown with the cache off too. Several changes in this file and in
These all look like genuine fixes, but there are no tests for any of them. When a format config sets Could you add translator unit tests and a few Iceberg query tests with the cache off? Cover
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added translator and cache-off query tests for |
||
| if (nameRef instanceof SchemaPath && expression instanceof ConstantExpression) { | ||
| String name = IcebergGroupScan.getPath((SchemaPath) nameRef); | ||
| return Expressions.greaterThanOrEqual(name, ((ConstantExpression<?>) expression).getValue()); | ||
|
|
@@ -124,14 +164,20 @@ public Expression visitFunctionCall(FunctionCall call, Void value) throws Runtim | |
| Expression expression = call.args().get(1).accept(this, null); | ||
| if (nameRef instanceof SchemaPath && expression instanceof ConstantExpression) { | ||
| String name = IcebergGroupScan.getPath((SchemaPath) nameRef); | ||
| return Expressions.notEqual(name, ((ConstantExpression<?>) expression).getValue()); | ||
| return rejectNull(name, Expressions.notEqual(name, ((ConstantExpression<?>) expression).getValue())); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static Expression rejectNull(String name, Expression predicate) { | ||
| // Iceberg 0.12.1 orders null before non-null values when evaluating residuals. | ||
| // SQL comparisons must reject null, including <, <= and !=. | ||
| return Expressions.and(Expressions.notNull(name), predicate); | ||
| } | ||
|
|
||
| @Override | ||
| public Expression visitFloatConstant(ValueExpressions.FloatExpression fExpr, Void value) throws RuntimeException { | ||
| return new ConstantExpression<>(fExpr.getFloat()); | ||
|
|
@@ -174,17 +220,17 @@ public Expression visitVarDecimalConstant(ValueExpressions.VarDecimalExpression | |
|
|
||
| @Override | ||
| public Expression visitDateConstant(ValueExpressions.DateExpression dateExpr, Void value) throws RuntimeException { | ||
| return new ConstantExpression<>(dateExpr.getDate()); | ||
| return new ConstantExpression<>(Math.toIntExact(Math.floorDiv(dateExpr.getDate(), 86_400_000L))); | ||
| } | ||
|
|
||
| @Override | ||
| public Expression visitTimeConstant(ValueExpressions.TimeExpression timeExpr, Void value) throws RuntimeException { | ||
| return new ConstantExpression<>(timeExpr.getTime()); | ||
| return new ConstantExpression<>((long) timeExpr.getTime() * 1_000L); | ||
| } | ||
|
|
||
| @Override | ||
| public Expression visitTimeStampConstant(ValueExpressions.TimeStampExpression timestampExpr, Void value) throws RuntimeException { | ||
| return new ConstantExpression<>(timestampExpr.getTimeStamp()); | ||
| return new ConstantExpression<>(Math.multiplyExact(timestampExpr.getTimeStamp(), 1_000L)); | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,8 @@ | |
| import org.apache.calcite.rex.RexLiteral; | ||
| import org.apache.calcite.rex.RexNode; | ||
| import org.apache.calcite.util.Util; | ||
| import org.apache.drill.common.FunctionNames; | ||
| import org.apache.drill.common.expression.FunctionCallFactory; | ||
| import org.apache.drill.common.expression.LogicalExpression; | ||
| import org.apache.drill.common.expression.SchemaPath; | ||
| import org.apache.drill.exec.physical.base.GroupScan; | ||
|
|
@@ -46,6 +48,7 @@ | |
|
|
||
| import java.io.IOException; | ||
| import java.math.BigDecimal; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
|
|
@@ -67,6 +70,10 @@ public void implement(PluginFilterRel filter) throws IOException { | |
| new DrillParseContext(PrelUtil.getPlannerSettings(filter.getCluster().getPlanner())), | ||
| filter.getInput(), | ||
| condition); | ||
| if (groupScan.getCondition() != null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to the translator comment: combining with the existing condition is the right behaviour, but it also affects queries with the cache off. A test with two stacked filters would pin it down.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an implementor regression test for two stacked filters, such as |
||
| expression = FunctionCallFactory.createBooleanOperator(FunctionNames.AND, | ||
| Arrays.asList(groupScan.getCondition(), expression)); | ||
| } | ||
| groupScan = groupScan.toBuilder().condition(expression).build(); | ||
| } | ||
|
|
||
|
|
@@ -98,6 +105,11 @@ public boolean canImplement(Filter filter) { | |
| GroupScan scan = findGroupScan(filter); | ||
| if (scan instanceof IcebergGroupScan) { | ||
| IcebergGroupScan groupScan = (IcebergGroupScan) scan; | ||
| // This rule replaces Drill's Filter. Without Iceberg residuals, file | ||
| // pruning alone cannot guarantee that every returned row matches. | ||
| if (Boolean.TRUE.equals(groupScan.getFormatPlugin().getConfig().getIgnoreResiduals())) { | ||
| return false; | ||
| } | ||
| // ensures that expression compatible with table schema | ||
| expression = Binder.bind(groupScan.getTableScan().schema().asStruct(), expression, true); | ||
| } else { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Correctness] A change to the provided schema doesn't invalidate the entry.
The version is
uuid:currentSchemaId. However, the cachedIcebergGroupScanalso carries Drill's provided schema from.drill.schemawhenstore.table.use_schema_file=true. AfterCREATE OR REPLACE SCHEMA ... FOR TABLE, hits keep using the old types and casts. The options fingerprint only covers the flag, not the file contents.Could you fold a hash of the provided schema into the version (or the schema file's modification time and length), or return null when a schema provider is present?
Separately, tables without a UUID silently never cache. That's safe, but worth a line in the plugin guide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Provided schema contents now contribute to table compatibility. For example, changing
id VARCHARtoid BIGINTinvalidates the old entry. Added tests with and without provided schemas, including native schema changes. Tables without an Iceberg UUID still bypass caching; this is now explicit in the plugin guide.