-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[CALCITE-7627] Enumerable DML should reject assignments that may lose data #5211
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: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -17,17 +17,66 @@ | |
| package org.apache.calcite.adapter.enumerable; | ||
|
|
||
| import org.apache.calcite.plan.Convention; | ||
| import org.apache.calcite.plan.RelOptCluster; | ||
| import org.apache.calcite.plan.RelTraitSet; | ||
| import org.apache.calcite.prepare.RelOptTableImpl; | ||
| import org.apache.calcite.rel.RelNode; | ||
| import org.apache.calcite.rel.convert.ConverterRule; | ||
| import org.apache.calcite.rel.core.TableModify; | ||
| import org.apache.calcite.rel.logical.LogicalTableModify; | ||
| import org.apache.calcite.rel.type.RelDataType; | ||
| import org.apache.calcite.rel.type.RelDataTypeField; | ||
| import org.apache.calcite.rex.RexBuilder; | ||
| import org.apache.calcite.rex.RexNode; | ||
| import org.apache.calcite.rex.RexProgram; | ||
| import org.apache.calcite.rex.RexUtil; | ||
| import org.apache.calcite.schema.ModifiableTable; | ||
| import org.apache.calcite.sql.fun.SqlInternalOperators; | ||
| import org.apache.calcite.sql.fun.SqlStdOperatorTable; | ||
| import org.apache.calcite.sql.type.SqlTypeName; | ||
| import org.apache.calcite.sql.type.SqlTypeUtil; | ||
|
|
||
| import org.checkerframework.checker.nullness.qual.Nullable; | ||
|
|
||
| /** Planner rule that converts a {@link LogicalTableModify} to an {@link EnumerableTableModify}. | ||
| * You may provide a custom config to convert other nodes that extend {@link TableModify}. | ||
| import java.math.BigDecimal; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import static java.util.Objects.requireNonNull; | ||
|
|
||
| /** Planner rule that converts a {@link LogicalTableModify} to an | ||
| * {@link EnumerableTableModify}. | ||
| * | ||
| * <p>For INSERT and UPDATE, the rule adds assignment checks of the following | ||
| * form: | ||
| * | ||
| * <blockquote><pre>{@code | ||
| * LogicalTableModify | ||
| * input | ||
| * | ||
| * EnumerableTableModify | ||
| * EnumerableCalc( | ||
| * condition=[ | ||
| * AND( | ||
| * $THROW_UNLESS( | ||
| * IS_NOT_FALSE( | ||
| * LESS_THAN_OR_EQUAL(CHAR_LENGTH($0), targetPrecision)), ...), | ||
| * $THROW_UNLESS( | ||
| * IS_NOT_FALSE( | ||
| * LESS_THAN_OR_EQUAL(OCTET_LENGTH($1), targetPrecision)), ...))], | ||
| * projects=[ | ||
| * $0, | ||
| * $1, | ||
| * CAST($2 AS targetType)]) | ||
| * enumerable input | ||
| * }</pre></blockquote> | ||
| * | ||
| * <p>Character and binary values are checked by length because narrowing casts | ||
| * may truncate them. Exact numeric values use a target cast, which throws on | ||
| * overflow. | ||
| * | ||
| * <p>You may provide a custom config to convert other nodes that extend | ||
| * {@link TableModify}. | ||
| * | ||
| * @see EnumerableRules#ENUMERABLE_TABLE_MODIFICATION_RULE */ | ||
| public class EnumerableTableModifyRule extends ConverterRule { | ||
|
|
@@ -42,20 +91,91 @@ | |
| super(config); | ||
| } | ||
|
|
||
| @Override public @Nullable RelNode convert(RelNode rel) { | ||
|
Check failure on line 94 in core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java
|
||
| final TableModify modify = (TableModify) rel; | ||
| final RelOptCluster cluster = modify.getCluster(); | ||
| final ModifiableTable modifiableTable = | ||
| modify.getTable().unwrap(ModifiableTable.class); | ||
| if (modifiableTable == null) { | ||
| return null; | ||
| } | ||
| final RelTraitSet traitSet = | ||
| modify.getTraitSet().replace(EnumerableConvention.INSTANCE); | ||
| RelNode input = convert(modify.getInput(), traitSet); | ||
| if (modify.isInsert() || modify.isUpdate()) { | ||
| // INSERT assigns stored columns; UPDATE assigns columns in the SET list. | ||
| RelDataType assignmentType = modify.isInsert() | ||
| ? RelOptTableImpl.realRowType(modify.getTable()) | ||
| : modify.getCatalogReader().createTypeFromProjection( | ||
| modify.getTable().getRowType(), | ||
| requireNonNull(modify.getUpdateColumnList(), "updateColumnList")); | ||
| if (modify.isFlattened()) { | ||
| // TableModify flattens its input, so flatten the target fields too. | ||
| assignmentType = | ||
| SqlTypeUtil.flattenRecordType(cluster.getTypeFactory(), assignmentType, null); | ||
| } | ||
|
|
||
| final RexBuilder rexBuilder = cluster.getRexBuilder(); | ||
| final List<RexNode> projects = | ||
| new ArrayList<>(rexBuilder.identityProjects(input.getRowType())); | ||
| final List<RexNode> checks = new ArrayList<>(); | ||
| // UPDATE appends SET values to the old row; INSERT has only new values. | ||
| final int assignmentOffset = projects.size() - assignmentType.getFieldCount(); | ||
| for (RelDataTypeField field : assignmentType.getFieldList()) { | ||
|
Check warning on line 124 in core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java
|
||
| final int sourceOrdinal = assignmentOffset + field.getIndex(); | ||
| final RexNode source = projects.get(sourceOrdinal); | ||
| final RelDataType targetType = field.getType(); | ||
| final SqlTypeName targetName = targetType.getSqlTypeName(); | ||
| if (SqlTypeUtil.inCharOrBinaryFamilies(targetType)) { | ||
| if (targetType.getPrecision() < 0) { | ||
| continue; | ||
| } | ||
| // Check character and binary lengths because their casts may truncate. | ||
|
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. I think that a nicer solution would be to have a way for users to plug-in a function to convert each value.
Member
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. Thanks for reviewing. Just to make sure I understand: are you suggesting a hook for each target type that can validate or convert a value before it is assigned to the target column? Would you expect the generated code for EnumerableTableModify to call this hook at execution time, or would the hook build a Rex expression in the input plan?
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. Yes, some kind of hook would be nice. I expect different systems have different validation strategies for what is legal data. But I don't know how the hook can be described in a generic way and what API one could use to specify the hooks. This calls for a discussion in JIRA. Perhaps the hook is just this rule: EnumerableTableModify; people can plug in different implementations? Perhaps the rule can take as parameter an Interface which provides a hook for each type?
Member
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. Understood. I’ll summarize the alternatives in JIRA so we can discuss the extension point there before changing the PR. |
||
| final RexNode length = | ||
| rexBuilder.makeCall(SqlTypeUtil.inCharFamily(targetType) | ||
| ? SqlStdOperatorTable.CHAR_LENGTH | ||
| : SqlStdOperatorTable.OCTET_LENGTH, | ||
| source); | ||
| final RexNode fits = | ||
| rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, length, | ||
| rexBuilder.makeExactLiteral( | ||
| BigDecimal.valueOf(targetType.getPrecision()))); | ||
| // IS_NOT_FALSE lets NULL pass this length check. | ||
| final RexNode valid = | ||
| rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_FALSE, fits); | ||
| checks.add( | ||
| rexBuilder.makeCall(SqlInternalOperators.THROW_UNLESS, | ||
| valid, rexBuilder.makeLiteral("Value exceeds precision " | ||
| + targetType.getPrecision() + " of " | ||
| + targetType.getFullTypeString()))); | ||
| } else { | ||
| if (targetName == SqlTypeName.DECIMAL) { | ||
| // A runtime BigDecimal may exceed its declared precision. | ||
| if (targetType.getPrecision() < 0 || targetType.getScale() < 0) { | ||
| continue; | ||
| } | ||
| } else if (!SqlTypeUtil.isExactNumeric(targetType) | ||
| || source.getType().getSqlTypeName() == targetName) { | ||
| continue; | ||
| } | ||
| // Exact numeric casts reject overflow and produce the target value. | ||
| projects.set(sourceOrdinal, rexBuilder.makeCast(targetType, source)); | ||
| } | ||
| } | ||
| if (!checks.isEmpty() || !RexUtil.isIdentity(projects, input.getRowType())) { | ||
| // RexProgram has one condition, so combine all length checks. | ||
| input = | ||
| EnumerableCalc.create( | ||
| input, RexProgram.create(input.getRowType(), projects, | ||
| RexUtil.composeConjunction(rexBuilder, checks, true), | ||
| input.getRowType().getFieldNames(), rexBuilder)); | ||
| } | ||
| } | ||
| return new EnumerableTableModify( | ||
| modify.getCluster(), traitSet, | ||
| cluster, traitSet, | ||
| modify.getTable(), | ||
| modify.getCatalogReader(), | ||
| convert(modify.getInput(), traitSet), | ||
| input, | ||
| modify.getOperation(), | ||
| modify.getUpdateColumnList(), | ||
| modify.getSourceExpressionList(), | ||
|
|
||
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.
Had no idea that this expression exists