Skip to content
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

schemachanger: Implement ADD CONSTRAINT NOT VALID #96243

Merged
merged 3 commits into from
Feb 6, 2023

Conversation

Xiang-Gu
Copy link
Contributor

@Xiang-Gu Xiang-Gu commented Jan 30, 2023

This PR implements the following three statements in the declarative schema changer:

  • ALTER TABLE ... ADD CHECK ... NOT VALID
  • ALTER TABLE ... ADD UNIQUE WITHOUT INDEX ... NOT VALID
  • ALTER TABLE ... ADD FOREIGN KEY ... NOT VALID

The idea is to introduce a new element for each statement. That element is like simple
dependent and will transition between ABSENT and PUBLIC directly.

Epic: None

@cockroach-teamcity
Copy link
Member

This change is Reviewable

@Xiang-Gu Xiang-Gu force-pushed the implement-add-check-not-valid branch 6 times, most recently from a6d5d35 to a1b95a1 Compare February 1, 2023 20:51
@Xiang-Gu Xiang-Gu changed the title WIP: schemachanger: Implement ADD CHECK NOT VALID WIP: schemachanger: Implement ADD CONSTRAINT NOT VALID Feb 1, 2023
@Xiang-Gu Xiang-Gu force-pushed the implement-add-check-not-valid branch 4 times, most recently from f0c2ff6 to d654981 Compare February 2, 2023 17:03
@Xiang-Gu
Copy link
Contributor Author

Xiang-Gu commented Feb 2, 2023

This is ready for a look!

@Xiang-Gu Xiang-Gu changed the title WIP: schemachanger: Implement ADD CONSTRAINT NOT VALID schemachanger: Implement ADD CONSTRAINT NOT VALID Feb 2, 2023
@Xiang-Gu Xiang-Gu marked this pull request as ready for review February 2, 2023 17:41
@Xiang-Gu Xiang-Gu requested a review from a team February 2, 2023 17:41
Copy link
Collaborator

@fqazi fqazi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 35 of 35 files at r1, 24 of 24 files at r2, 24 of 24 files at r3, all commit messages.
Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @Xiang-Gu)


-- commits line 15 at r1:
*filters


pkg/sql/schemachanger/scexec/scmutationexec/constraint.go line 89 at r1 (raw file):

}

func (i *immediateVisitor) MakeAbsentCheckConstraintNotValidPublic(

Nity: Maybe call these unvalidated

Code quote:

MakeAbsentCheckConstraintNotValidPublic

pkg/sql/schemachanger/scpb/elements.proto line 79 at r2 (raw file):

  RowLevelTTL row_level_ttl = 29 [(gogoproto.customname) = "RowLevelTTL", (gogoproto.moretags) = "parent:\"Table\""];
  CheckConstraintNotValid check_constraint_not_valid = 170 [(gogoproto.moretags) = "parent:\"Table\""];
  UniqueWithoutIndexConstraintNotValid unique_without_index_constraint_not_valid = 171 [(gogoproto.moretags) = "parent:\"Table\""];

I guess for VALIDATE CONSTRAINT command...we are going to drop and add elements? Trying to reason about how that transformation will work in our current model, since we don't want a limited number of operations


pkg/sql/schemachanger/scplan/internal/opgen/opgen_foreign_key_constraint_not_valid.go line 41 at r3 (raw file):

			to(scpb.Status_ABSENT,
				// TODO(postamar): remove revertibility constraint when possible
				revertible(false),

Should this be considered revertible?


pkg/sql/schemachanger/scplan/internal/rules/current/dep_drop_constraint.go line 46 at r1 (raw file):

			return rel.Clauses{
				from.TypeFilter(rulesVersionKey, isConstraintDependent),
				to.TypeFilter(rulesVersionKey, isNonIndexBackedComplexConstraint),

Can we share these with the Or operation?

@Xiang-Gu Xiang-Gu force-pushed the implement-add-check-not-valid branch from d654981 to 2bcab3c Compare February 2, 2023 23:30
@postamar
Copy link
Contributor

postamar commented Feb 3, 2023

I guess for VALIDATE CONSTRAINT command...we are going to drop and add elements? Trying to reason about how that transformation will work in our current model, since we don't want a limited number of operations

Yes. The elements' terminal state outside of a schema change has to be either ABSENT or PUBLIC. We can't re-use the CheckConstraint element and leave it in WRITE_ONLY. So, VALIDATE CONSTRAINT takes down the NotValid constraint element in favour of its vanilla equivalent.

Copy link
Contributor

@postamar postamar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is close. I'm very pleased to see that the impact on the rules is very benign. The main theme of my comments is around tightening things:

  • tighten the ops by re-using the existing ones
  • tighten the type predicates used in the rules

The test failures seem unconcerning except for that backup test failure. I suspect that you need to add some more descriptor ID rewrite support in pkg/sql/catalog/rewrite.

I won't be around to witness it happening but I hope VALIDATE CONSTRAINT to be equally painless.

ConstraintID: op.ConstraintID,
}
tbl.Checks = append(tbl.Checks, ck)
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you instead repurpose the MakeAbsentCheckConstraintPublic op to accept an extra Validity field? These ops are the same otherwise. Same comment for other NotValid ops.

@@ -75,6 +75,9 @@ message ElementProto {
ForeignKeyConstraint foreign_key_constraint = 27 [(gogoproto.moretags) = "parent:\"Table\""];
TableComment table_comment = 28 [(gogoproto.moretags) = "parent:\"Table, View, Sequence\""];
RowLevelTTL row_level_ttl = 29 [(gogoproto.customname) = "RowLevelTTL", (gogoproto.moretags) = "parent:\"Table\""];
CheckConstraintNotValid check_constraint_not_valid = 170 [(gogoproto.moretags) = "parent:\"Table\""];
UniqueWithoutIndexConstraintNotValid unique_without_index_constraint_not_valid = 171 [(gogoproto.moretags) = "parent:\"Table\""];
ForeignKeyConstraintNotValid foreign_key_constraint_not_valid = 172 [(gogoproto.moretags) = "parent:\"Table\""];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can you list these next to their vanilla equivalents? This is slightly harder to read.

repeated uint32 column_ids = 3 [(gogoproto.customname) = "ColumnIDs", (gogoproto.casttype) = "github.com/cockroachdb/cockroach/pkg/sql/sem/catid.ColumnID"];
// Predicate, if non-nil, means a partial uniqueness constraint.
Expression predicate = 4 [(gogoproto.customname) = "Predicate"];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meta-comment: I mentioned something about re-using the same ops in another, so you might expect me to do the same here and say we need to embed some common message, but no, this doesn't bother me. The reason is that I only dislike copy-pasting of code when it introduces opportunities for bugs. It's quite possible that in the future we find a bug in one of those ops, fix that op, and forget about the other very similar op which also has the same bug, the change sails through review because review tooling only shows the diff, etc... On the other hand, these element definitions are more future-proof, they're unlikely to change much over time, they're right next to each other in the file so unlikely to be overlooked, etc. The same reasoning applies for the op-gen definitions, because they don't tend to change much. If they do, we can, later on, factor the code there by introducing helper functions.

//
// We call them "complex" constraint in contrast to simple constraint that
// does not have an intermediate state.
func isNonIndexBackedComplexConstraint(e scpb.Element) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name "complex" is not very meaningful. These constraints are subject to the 2-version invariant and have intermediate states. The NOT VALID constraints don't. One can distinguish the latter easily with the conjunction And(isConstraint, Not(isSubjectTo2VersionInvariant)).

When building a grammar like this try to make the existing constructs as powerful as possible and only add new ones if you can't avoid it. The more you add, the bigger the maintenance burden, and the lesser the expressivity and easiness to reason about.

// TODO (xiang): UniqueWithoutIndex and UniqueWithoutIndexNotValid should
// also be treated as cross-descriptor constraint because its partial predicate
// can references other descriptors.
func isNonIndexBackedCrossDescriptorConstraint(e scpb.Element) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said in my previous comment, I don't think you need this either. case *scpb.CheckConstraint, *scpb.UniqueWithoutIndexConstraint, *scpb.ForeignKeyConstraint should verify isSubjectTo2VersionInvariant and the others not.



test
ALTER TABLE t ADD CHECK (i > 0) NOT VALID;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These end-to-end tests are quite expensive because they balloon into TestRollback and TestBackup tests. I don't think there's much value in having one for each three non-index constraint types, just pick one.

Unrelated but this test case is nice in that it sets the stage for a VALIDATE CONSTRAINT case to be appended to the file later on.

@Xiang-Gu Xiang-Gu force-pushed the implement-add-check-not-valid branch from 2bcab3c to 60acc70 Compare February 3, 2023 21:51
Copy link
Contributor Author

@Xiang-Gu Xiang-Gu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@postamar Thanks for your prompt review.

  1. yes, I've changed it to re-use existing operations so now each XxxUnvalidate element share the same set of operation as their vanilla counterpart;
  2. I agree with you that I don't like "complexConstraint" filter either, so I got rid of it and instead implement it in the subjectToTwoVersionInvariant filter.
  3. The backup failure is due to the fact that FK constraintID in the referencing table does not match that in the referencing table so we do need to grab the FK name first from the referencing table and use it to identify FKs in the referenced table.

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @fqazi and @postamar)


pkg/sql/schemachanger/scexec/scmutationexec/constraint.go line 89 at r1 (raw file):

Previously, fqazi (Faizan Qazi) wrote…

Nity: Maybe call these unvalidated

Indeed that's a better name. Done!


pkg/sql/schemachanger/scexec/scmutationexec/constraint.go line 111 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

Can you instead repurpose the MakeAbsentCheckConstraintPublic op to accept an extra Validity field? These ops are the same otherwise. Same comment for other NotValid ops.

Done


pkg/sql/schemachanger/scpb/elements.proto line 79 at r2 (raw file):

Previously, fqazi (Faizan Qazi) wrote…

I guess for VALIDATE CONSTRAINT command...we are going to drop and add elements? Trying to reason about how that transformation will work in our current model, since we don't want a limited number of operations

yes, VALIDATE CONSTRAINT is just as simple as dropping the xxxUnvalidated element and add a xxx element. We will need to add rules to ensure the xxxUnvalidated element does not get dropped until xxx element is added.


pkg/sql/schemachanger/scpb/elements.proto line 80 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

nit: Can you list these next to their vanilla equivalents? This is slightly harder to read.

done


pkg/sql/schemachanger/scpb/elements.proto line 367 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

meta-comment: I mentioned something about re-using the same ops in another, so you might expect me to do the same here and say we need to embed some common message, but no, this doesn't bother me. The reason is that I only dislike copy-pasting of code when it introduces opportunities for bugs. It's quite possible that in the future we find a bug in one of those ops, fix that op, and forget about the other very similar op which also has the same bug, the change sails through review because review tooling only shows the diff, etc... On the other hand, these element definitions are more future-proof, they're unlikely to change much over time, they're right next to each other in the file so unlikely to be overlooked, etc. The same reasoning applies for the op-gen definitions, because they don't tend to change much. If they do, we can, later on, factor the code there by introducing helper functions.

ack, thanks a lot for the advice!


pkg/sql/schemachanger/scplan/internal/rules/current/helpers.go line 218 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

This name "complex" is not very meaningful. These constraints are subject to the 2-version invariant and have intermediate states. The NOT VALID constraints don't. One can distinguish the latter easily with the conjunction And(isConstraint, Not(isSubjectTo2VersionInvariant)).

When building a grammar like this try to make the existing constructs as powerful as possible and only add new ones if you can't avoid it. The more you add, the bigger the maintenance burden, and the lesser the expressivity and easiness to reason about.

The thing is we defined isSubjectTo2VersionInvariant based on the following three predicates:

return isIndex(e) || isColumn(e) || isNonIndexBackedComplexConstraint(e)

I don't particularly like the name "complex" here either. So let's get rid of this filter and instead implement isSubjectTo2VersionInvariant as a building block:

if isIndex(e) || isColumn(e) {
    return true
}
switch e.(type) {
    case Check, FK, UWI, NotNull:
        return true
}
return false

pkg/sql/schemachanger/scplan/internal/rules/current/helpers.go line 238 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

Like I said in my previous comment, I don't think you need this either. case *scpb.CheckConstraint, *scpb.UniqueWithoutIndexConstraint, *scpb.ForeignKeyConstraint should verify isSubjectTo2VersionInvariant and the others not.

Done.


pkg/sql/schemachanger/scplan/internal/opgen/opgen_foreign_key_constraint_not_valid.go line 41 at r3 (raw file):

Previously, fqazi (Faizan Qazi) wrote…

Should this be considered revertible?

you're right: it's okay to add back an invalidated constraint shortly after it's removed. All the rows added during that time won't violate anything when this invalidated constraint is added back.


pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_not_valid line 10 at r6 (raw file):

Previously, postamar (Marius Posta) wrote…

These end-to-end tests are quite expensive because they balloon into TestRollback and TestBackup tests. I don't think there's much value in having one for each three non-index constraint types, just pick one.

Unrelated but this test case is nice in that it sets the stage for a VALIDATE CONSTRAINT case to be appended to the file later on.

alright, I'll just keep the end-to-end test for check.

Not-valid constraints are like simple dependents: they don't have any
intermediate state so it can transition directly between public and
absent. This nature requires us not to enforce certains rules that are
enforced on the normal constraints. To that effect, we introduce the
concept/partition of "simple" vs. "complex" constraints:
  - "complex constraint": a constraint that needs to transition through
  an intermediate status;
  - "simple constraint": a constraint that does not need to transition
  through an intermediate status. They are the NOT VALID version of the
  complex constraints.

This commit also slightly modified the fitlers on constraints to be
more principled.
@Xiang-Gu Xiang-Gu force-pushed the implement-add-check-not-valid branch from 60acc70 to b9a0471 Compare February 3, 2023 21:59
Copy link
Collaborator

@fqazi fqazi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm: great work!

Reviewed 1 of 38 files at r4, 20 of 50 files at r7, 32 of 32 files at r10, 23 of 23 files at r11, 25 of 25 files at r12, all commit messages.
Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (waiting on @postamar and @Xiang-Gu)

@Xiang-Gu
Copy link
Contributor Author

Xiang-Gu commented Feb 6, 2023

bors r+

@craig
Copy link
Contributor

craig bot commented Feb 6, 2023

Build succeeded:

@craig craig bot merged commit b031933 into cockroachdb:master Feb 6, 2023
@Xiang-Gu Xiang-Gu deleted the implement-add-check-not-valid branch February 6, 2023 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants