-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Add support for CHECK constraint in INSERT statement in engine and SPI #14964
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
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 |
|---|---|---|
|
|
@@ -115,7 +115,6 @@ | |
| import io.trino.sql.analyzer.Scope.AsteriskedIdentifierChainBasis; | ||
| import io.trino.sql.parser.ParsingException; | ||
| import io.trino.sql.parser.SqlParser; | ||
| import io.trino.sql.planner.DeterminismEvaluator; | ||
| import io.trino.sql.planner.ExpressionInterpreter; | ||
| import io.trino.sql.planner.PartitioningHandle; | ||
| import io.trino.sql.planner.ScopeAware; | ||
|
|
@@ -297,6 +296,7 @@ | |
| import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_FOUND; | ||
| import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_WINDOW; | ||
| import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS; | ||
| import static io.trino.spi.StandardErrorCode.INVALID_CHECK_CONSTRAINT; | ||
| import static io.trino.spi.StandardErrorCode.INVALID_COLUMN_REFERENCE; | ||
| import static io.trino.spi.StandardErrorCode.INVALID_COPARTITIONING; | ||
| import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; | ||
|
|
@@ -365,6 +365,8 @@ | |
| import static io.trino.sql.analyzer.SemanticExceptions.semanticException; | ||
| import static io.trino.sql.analyzer.TypeSignatureProvider.fromTypes; | ||
| import static io.trino.sql.analyzer.TypeSignatureTranslator.toTypeSignature; | ||
| import static io.trino.sql.planner.DeterminismEvaluator.containsCurrentTimeFunctions; | ||
| import static io.trino.sql.planner.DeterminismEvaluator.isDeterministic; | ||
| import static io.trino.sql.planner.ExpressionInterpreter.evaluateConstantExpression; | ||
| import static io.trino.sql.tree.BooleanLiteral.TRUE_LITERAL; | ||
| import static io.trino.sql.tree.DereferenceExpression.getQualifiedName; | ||
|
|
@@ -542,6 +544,7 @@ protected Scope visitInsert(Insert insert, Optional<Scope> scope) | |
| List<ColumnSchema> columns = tableSchema.getColumns().stream() | ||
| .filter(column -> !column.isHidden()) | ||
| .collect(toImmutableList()); | ||
| List<String> checkConstraints = tableSchema.getTableSchema().getCheckConstraints(); | ||
|
|
||
| for (ColumnSchema column : columns) { | ||
| if (!accessControl.getColumnMasks(session.toSecurityContext(), targetTable, column.getName(), column.getType()).isEmpty()) { | ||
|
|
@@ -551,7 +554,12 @@ protected Scope visitInsert(Insert insert, Optional<Scope> scope) | |
|
|
||
| Map<String, ColumnHandle> columnHandles = metadata.getColumnHandles(session, targetTableHandle.get()); | ||
| List<Field> tableFields = analyzeTableOutputFields(insert.getTable(), targetTable, tableSchema, columnHandles); | ||
| analyzeFiltersAndMasks(insert.getTable(), targetTable, targetTableHandle, tableFields, session.getIdentity().getUser()); | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), new RelationType(tableFields)) | ||
| .build(); | ||
| analyzeFiltersAndMasks(insert.getTable(), targetTable, new RelationType(tableFields), accessControlScope); | ||
| analyzeCheckConstraints(insert.getTable(), targetTable, accessControlScope, checkConstraints); | ||
| analysis.registerTable(insert.getTable(), targetTableHandle, targetTable, session.getIdentity().getUser(), accessControlScope); | ||
|
|
||
| List<String> tableColumns = columns.stream() | ||
| .map(ColumnSchema::getName) | ||
|
|
@@ -801,7 +809,12 @@ protected Scope visitDelete(Delete node, Optional<Scope> scope) | |
|
|
||
| analysis.setUpdateType("DELETE"); | ||
| analysis.setUpdateTarget(tableName, Optional.of(table), Optional.empty()); | ||
| analyzeFiltersAndMasks(table, tableName, Optional.of(handle), analysis.getScope(table).getRelationType(), session.getIdentity().getUser()); | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), analysis.getScope(table).getRelationType()) | ||
| .build(); | ||
| analyzeFiltersAndMasks(table, tableName, analysis.getScope(table).getRelationType(), accessControlScope); | ||
| analyzeCheckConstraints(table, tableName, accessControlScope, tableSchema.getTableSchema().getCheckConstraints()); | ||
| analysis.registerTable(table, Optional.of(handle), tableName, session.getIdentity().getUser(), accessControlScope); | ||
|
|
||
| createMergeAnalysis(table, handle, tableSchema, tableScope, tableScope, ImmutableList.of()); | ||
|
|
||
|
|
@@ -2188,7 +2201,12 @@ protected Scope visitTable(Table table, Optional<Scope> scope) | |
|
|
||
| List<Field> outputFields = fields.build(); | ||
|
|
||
| analyzeFiltersAndMasks(table, targetTableName, tableHandle, outputFields, session.getIdentity().getUser()); | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), new RelationType(outputFields)) | ||
| .build(); | ||
| analyzeFiltersAndMasks(table, targetTableName, new RelationType(outputFields), accessControlScope); | ||
| analyzeCheckConstraints(table, targetTableName, accessControlScope, tableSchema.getTableSchema().getCheckConstraints()); | ||
| analysis.registerTable(table, tableHandle, targetTableName, session.getIdentity().getUser(), accessControlScope); | ||
|
|
||
| Scope tableScope = createAndAssignScope(table, scope, outputFields); | ||
|
|
||
|
|
@@ -2208,17 +2226,8 @@ private void checkStorageTableNotRedirected(QualifiedObjectName source) | |
| }); | ||
| } | ||
|
|
||
| private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Optional<TableHandle> tableHandle, List<Field> fields, String authorization) | ||
| { | ||
| analyzeFiltersAndMasks(table, name, tableHandle, new RelationType(fields), authorization); | ||
| } | ||
|
|
||
| private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Optional<TableHandle> tableHandle, RelationType relationType, String authorization) | ||
| private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, RelationType relationType, Scope accessControlScope) | ||
|
Member
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. Why did this change? It seems unrelated to this new feature.
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. The same
Member
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. Ok, that makes sense. Please add that short explanation to the commit message so it doesn't get lost. |
||
| { | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), relationType) | ||
| .build(); | ||
|
|
||
| for (int index = 0; index < relationType.getAllFieldCount(); index++) { | ||
| Field field = relationType.getFieldByIndex(index); | ||
| if (field.getName().isPresent()) { | ||
|
|
@@ -2232,8 +2241,14 @@ private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Optio | |
|
|
||
| accessControl.getRowFilters(session.toSecurityContext(), name) | ||
| .forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter)); | ||
| } | ||
|
|
||
| analysis.registerTable(table, tableHandle, name, authorization, accessControlScope); | ||
| private void analyzeCheckConstraints(Table table, QualifiedObjectName name, Scope accessControlScope, List<String> constraints) | ||
| { | ||
| for (String constraint : constraints) { | ||
| ViewExpression expression = new ViewExpression(session.getIdentity().getUser(), Optional.of(name.getCatalogName()), Optional.of(name.getSchemaName()), constraint); | ||
| analyzeCheckConstraint(table, name, accessControlScope, expression); | ||
| } | ||
| } | ||
|
|
||
| private boolean checkCanSelectFromColumn(QualifiedObjectName name, String column) | ||
|
|
@@ -2375,13 +2390,21 @@ private Scope createScopeForView( | |
|
|
||
| if (storageTable.isPresent()) { | ||
| List<Field> storageTableFields = analyzeStorageTable(table, viewFields, storageTable.get()); | ||
| analyzeFiltersAndMasks(table, name, storageTable, viewFields, session.getIdentity().getUser()); | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), new RelationType(viewFields)) | ||
| .build(); | ||
| analyzeFiltersAndMasks(table, name, new RelationType(viewFields), accessControlScope); | ||
| analysis.registerTable(table, storageTable, name, session.getIdentity().getUser(), accessControlScope); | ||
| analysis.addRelationCoercion(table, viewFields.stream().map(Field::getType).toArray(Type[]::new)); | ||
| // use storage table output fields as they contain ColumnHandles | ||
| return createAndAssignScope(table, scope, storageTableFields); | ||
| } | ||
|
|
||
| analyzeFiltersAndMasks(table, name, storageTable, viewFields, session.getIdentity().getUser()); | ||
| Scope accessControlScope = Scope.builder() | ||
| .withRelationType(RelationId.anonymous(), new RelationType(viewFields)) | ||
| .build(); | ||
| analyzeFiltersAndMasks(table, name, new RelationType(viewFields), accessControlScope); | ||
| analysis.registerTable(table, storageTable, name, session.getIdentity().getUser(), accessControlScope); | ||
| viewFields.forEach(field -> analysis.addSourceColumns(field, ImmutableSet.of(new SourceColumn(name, field.getName().orElseThrow())))); | ||
| analysis.registerNamedQuery(table, query); | ||
| return createAndAssignScope(table, scope, viewFields); | ||
|
|
@@ -3174,6 +3197,10 @@ protected Scope visitUpdate(Update update, Optional<Scope> scope) | |
| if (!accessControl.getRowFilters(session.toSecurityContext(), tableName).isEmpty()) { | ||
| throw semanticException(NOT_SUPPORTED, update, "Updating a table with a row filter is not supported"); | ||
| } | ||
| if (!tableSchema.getTableSchema().getCheckConstraints().isEmpty()) { | ||
| // TODO https://github.com/trinodb/trino/issues/15411 Add support for CHECK constraint to UPDATE statement | ||
| throw semanticException(NOT_SUPPORTED, update, "Updating a table with a check constraint is not supported"); | ||
|
Comment on lines
3201
to
3202
Member
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. Nice! |
||
| } | ||
|
|
||
| // TODO: how to deal with connectors that need to see the pre-image of rows to perform the update without | ||
| // flowing that data through the masking logic | ||
|
|
@@ -3301,6 +3328,10 @@ protected Scope visitMerge(Merge merge, Optional<Scope> scope) | |
| if (!accessControl.getRowFilters(session.toSecurityContext(), tableName).isEmpty()) { | ||
| throw semanticException(NOT_SUPPORTED, merge, "Cannot merge into a table with row filters"); | ||
| } | ||
| if (!tableSchema.getTableSchema().getCheckConstraints().isEmpty()) { | ||
| // TODO https://github.com/trinodb/trino/issues/15411 Add support for CHECK constraint to MERGE statement | ||
| throw semanticException(NOT_SUPPORTED, merge, "Cannot merge into a table with check constraints"); | ||
| } | ||
|
|
||
| Scope targetTableScope = analyzer.analyzeForUpdate(relation, scope, UpdateKind.MERGE); | ||
| Scope sourceTableScope = process(merge.getSource(), scope); | ||
|
|
@@ -4646,6 +4677,62 @@ private void analyzeRowFilter(String currentIdentity, Table table, QualifiedObje | |
| analysis.addRowFilter(table, expression); | ||
| } | ||
|
|
||
| private void analyzeCheckConstraint(Table table, QualifiedObjectName name, Scope scope, ViewExpression constraint) | ||
| { | ||
| Expression expression; | ||
| try { | ||
| expression = sqlParser.createExpression(constraint.getExpression(), createParsingOptions(session)); | ||
| } | ||
| catch (ParsingException e) { | ||
| throw new TrinoException(INVALID_CHECK_CONSTRAINT, extractLocation(table), format("Invalid check constraint for '%s': %s", name, e.getErrorMessage()), e); | ||
| } | ||
|
|
||
| verifyNoAggregateWindowOrGroupingFunctions(session, metadata, expression, format("Check constraint for '%s'", name)); | ||
|
|
||
| ExpressionAnalysis expressionAnalysis; | ||
| try { | ||
| Identity filterIdentity = Identity.forUser(constraint.getIdentity()) | ||
| .withGroups(groupProvider.getGroups(constraint.getIdentity())) | ||
| .build(); | ||
| expressionAnalysis = ExpressionAnalyzer.analyzeExpression( | ||
| createViewSession(constraint.getCatalog(), constraint.getSchema(), filterIdentity, session.getPath()), | ||
| plannerContext, | ||
| statementAnalyzerFactory, | ||
| accessControl, | ||
| scope, | ||
| analysis, | ||
| expression, | ||
| warningCollector, | ||
| correlationSupport); | ||
| } | ||
| catch (TrinoException e) { | ||
| throw new TrinoException(e::getErrorCode, extractLocation(table), format("Invalid check constraint for '%s': %s", name, e.getRawMessage()), e); | ||
| } | ||
|
|
||
| // Ensure that the expression doesn't contain non-deterministic functions. This should be "retrospectively deterministic" per SQL standard. | ||
| if (!isDeterministic(expression, this::getResolvedFunction)) { | ||
|
Member
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. EXPRESSION_NOT_IN_DISTINCT is the wrong error code for this. |
||
| throw semanticException(INVALID_CHECK_CONSTRAINT, expression, "Check constraint expression should be deterministic"); | ||
| } | ||
| if (containsCurrentTimeFunctions(expression)) { | ||
| throw semanticException(INVALID_CHECK_CONSTRAINT, expression, "Check constraint expression should not contain temporal expression"); | ||
| } | ||
|
|
||
| analysis.recordSubqueries(expression, expressionAnalysis); | ||
|
|
||
| Type actualType = expressionAnalysis.getType(expression); | ||
| if (!actualType.equals(BOOLEAN)) { | ||
| TypeCoercion coercion = new TypeCoercion(plannerContext.getTypeManager()::getType); | ||
|
|
||
| if (!coercion.canCoerce(actualType, BOOLEAN)) { | ||
| throw new TrinoException(TYPE_MISMATCH, extractLocation(table), format("Expected check constraint for '%s' to be of type BOOLEAN, but was %s", name, actualType), null); | ||
| } | ||
|
|
||
| analysis.addCoercion(expression, BOOLEAN, coercion.isTypeOnlyCoercion(actualType, BOOLEAN)); | ||
|
Member
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. Is this coercion getting applied? |
||
| } | ||
|
|
||
| analysis.addCheckConstraints(table, expression); | ||
| } | ||
|
|
||
| private void analyzeColumnMask(String currentIdentity, Table table, QualifiedObjectName tableName, Field field, Scope scope, ViewExpression mask) | ||
| { | ||
| String column = field.getName().orElseThrow(); | ||
|
|
@@ -5031,7 +5118,7 @@ private void verifySelectDistinct(QuerySpecification node, List<Expression> orde | |
| } | ||
|
|
||
| for (Expression expression : orderByExpressions) { | ||
| if (!DeterminismEvaluator.isDeterministic(expression, this::getResolvedFunction)) { | ||
| if (!isDeterministic(expression, this::getResolvedFunction)) { | ||
| throw semanticException(EXPRESSION_NOT_IN_DISTINCT, expression, "Non deterministic ORDER BY expression is not supported with SELECT DISTINCT"); | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.