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

opt: add FK checks for UPDATE statements #39751

Merged
merged 1 commit into from
Aug 27, 2019
Merged

Conversation

justinj
Copy link
Contributor

@justinj justinj commented Aug 19, 2019

Been trying to get this in a reviewable state for a while—still not quite perfect but gotta get it out the door, I think. Going to have to rebase when #39462 goes in to get some better testing coverage.

Release note: None

@justinj justinj requested a review from a team as a code owner August 19, 2019 20:01
@cockroach-teamcity
Copy link
Member

This change is Reviewable

Copy link
Collaborator

@rytaft rytaft left a comment

Choose a reason for hiding this comment

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

Reviewed 21 of 21 files at r1.
Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @justinj and @RaduBerinde)


pkg/sql/opt_catalog.go, line 597 at r1 (raw file):

				deleteAction: fk.OnDelete,
				updateAction: fk.OnUpdate,
				id:           cat.StableID(pair(int(fk.Index), int(idxDesc.ID))),

did you mean for these elements in the pair to be in the opposite order as the one above?

(and is this id field even being used anymore)?


pkg/sql/logictest/testdata/logic_test/fk_opt, line 173 at r1 (raw file):


# This case illustrates why it's insufficient to just check the "old" values,
# even in the absence of temporary unique constraint violations.

I don't understand this case....


pkg/sql/opt/optbuilder/mutation_builder.go, line 932 at r1 (raw file):

		deleteCols := make(opt.ColList, fk.ColumnCount())
		for i := range deleteCols {

[nit] i is shadowing the outer variable


pkg/sql/opt/optbuilder/mutation_builder.go, line 948 at r1 (raw file):

		input, cols := mb.makeFKInputScan(deletedFKCols)
		abort := mb.addDeletionCheck(i, input, cols, fk.DeleteReferenceAction())

it's a bit odd to return "abort" as a bool -- seems more idiomatic to return "ok" and check for "!ok"


pkg/sql/opt/optbuilder/mutation_builder.go, line 974 at r1 (raw file):

	oldRowCols := mb.fetchOrds

	// newRowCols is an array mapping column ordinals in each the effective new

each the -> each of the


pkg/sql/opt/optbuilder/mutation_builder.go, line 1022 at r1 (raw file):

		numCols := fk.ColumnCount()

		oldRowFKColOrdinals := make([]scopeOrdinal, fk.ColumnCount())

can use numCols here


pkg/sql/opt/optbuilder/mutation_builder.go, line 1024 at r1 (raw file):

		oldRowFKColOrdinals := make([]scopeOrdinal, fk.ColumnCount())
		newRowFKColOrdinals := make([]scopeOrdinal, fk.ColumnCount())
		for i := 0; i < numCols; i++ {

[nit] shadowing outer variable


pkg/sql/opt/optbuilder/mutation_builder.go, line 1025 at r1 (raw file):

		newRowFKColOrdinals := make([]scopeOrdinal, fk.ColumnCount())
		for i := 0; i < numCols; i++ {
			oldRowFKColOrdinals[i] = oldRowCols[fk.ReferencedColumnOrdinal(mb.tab, i)]

you could save this in a variable to avoid calling it twice


pkg/sql/opt/optbuilder/mutation_builder.go, line 1053 at r1 (raw file):

		outCols := make(opt.ColList, len(deletedFKCols))
		proj := memo.ProjectionsExpr{}
		for i := 0; i < len(deletedFKCols); i++ {

[nit] shadowing


pkg/sql/opt/optbuilder/mutation_builder.go, line 1075 at r1 (raw file):

// addInsertionCheck adds a FK check for rows which are added to a table.
// The input is the input to the mutation operator, and the insertCols is a

What input? I just see fkOrdinal


pkg/sql/opt/optbuilder/mutation_builder.go, line 1076 at r1 (raw file):

// addInsertionCheck adds a FK check for rows which are added to a table.
// The input is the input to the mutation operator, and the insertCols is a
// of the columns for the rows being inserted, indexed by their ordinal

a list of?


pkg/sql/opt/optbuilder/mutation_builder.go, line 1083 at r1 (raw file):

	var notNullInputCols opt.ColSet
	insertedFKCols := make(opt.ColList, numCols)
	for j := 0; j < numCols; j++ {

[nit] this can be i instead of j


pkg/sql/opt/optbuilder/testdata/fk-checks-update, line 73 at r1 (raw file):

      │                   └── variable: parent.p [type=int]
      └── f-k-checks-item: grandchild(c) -> child(c)
           └── semi-join (hash)

Is this check necessary since we're not updating c?

Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

Really cool to see this, and it looks less complicated than I expected.

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


pkg/sql/logictest/testdata/logic_test/fk_opt, line 173 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

I don't understand this case....

Each input row to the update contains both the new and the old value. In most cases you can assume that the old value is being removed (unless you're in one of the questionable cases above where different rows interact with each other). But in this case the old and the new value are the same.

Nice case Justin!


pkg/sql/opt/ops/mutation.opt, line 227 at r1 (raw file):

    # CreatorOp is the operator that generated this check, used to provide
    # better error messages.
    CreatorOp Operator

I'm not a fan of using opt operators as clues to what the SQL statement in an error should be. In fact, I have a branch where I remove mutationBuilder.op because I find it confusing - it's not always the op that ends up being built. I am replacing it with a statementTag string (which is usually the SQL StatementTag(), modulo some exception with UPSERT).

I think this field should similarly be a string too.

I was waiting for this PR to go in first so that I don't cause conflicts, but if it helps I can get that change out right now.


pkg/sql/opt/optbuilder/mutation_builder.go, line 966 at r1 (raw file):

	// Update we can emit both semi-joins and anti-joins.

	// The input to the Update operator consists of both the existing values for

[nit] could be more clear, eg "Each row input to the Update operator contains both the existing and the new value for each updated column"


pkg/sql/opt/optbuilder/mutation_builder.go, line 1034 at r1 (raw file):

		// of being updated _from_, minus the ones that were "added" by virtue of
		// being updated _to_.
		deletions := mb.b.factory.ConstructExcept(

Maybe mention that this could also be ExceptAll, it's not 100% clear to me which would be better in practice.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1216 at r1 (raw file):

}

func (mb *mutationBuilder) addDeletionCheck(

[nit] a comment would help


pkg/sql/opt/optbuilder/mutation_builder.go, line 1286 at r1 (raw file):

}

func (mb *mutationBuilder) projectOrdinals(

This definitely needs a comment


pkg/sql/opt/optbuilder/testdata/fk-checks-update, line 232 at r1 (raw file):

                     └── eq [type=bool]
                          ├── variable: c [type=int]
                          └── variable: grandchild2.c [type=int]

We need more tests here from a bunch of angles:

  • cases like the one above where we don't want to check FKs that aren't being modified

  • multi-column FKs, where only some of the columns are updated, including cases where the columns are in different families. By the way, we have an interesting TODO here: I can't find any code that figures out that if you modify column a and you have an outgoing FK on (a,b) you also need to scan b; I believe this currently happens automatically because we always have an index on a,b, but when we remove that requirement this won't be the case anymore. This can wait until 19.2 but it should be spelled out somewhere.

  • a self-referencing FK

@justinj justinj force-pushed the fk-update branch 3 times, most recently from da29b9a to 712f48f Compare August 22, 2019 20:12
Copy link
Contributor Author

@justinj justinj left a comment

Choose a reason for hiding this comment

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

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


pkg/sql/opt_catalog.go, line 597 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

did you mean for these elements in the pair to be in the opposite order as the one above?

(and is this id field even being used anymore)?

I think this is the right order—they're both in the order of (referencing, referenced), but this isn't necessary any more with the descriptor changes.


pkg/sql/logictest/testdata/logic_test/fk_opt, line 173 at r1 (raw file):

Previously, RaduBerinde wrote…

Each input row to the update contains both the new and the old value. In most cases you can assume that the old value is being removed (unless you're in one of the questionable cases above where different rows interact with each other). But in this case the old and the new value are the same.

Nice case Justin!

Thanks! I elaborated on the comment a bit.


pkg/sql/opt/ops/mutation.opt, line 227 at r1 (raw file):

Previously, RaduBerinde wrote…

I'm not a fan of using opt operators as clues to what the SQL statement in an error should be. In fact, I have a branch where I remove mutationBuilder.op because I find it confusing - it's not always the op that ends up being built. I am replacing it with a statementTag string (which is usually the SQL StatementTag(), modulo some exception with UPSERT).

I think this field should similarly be a string too.

I was waiting for this PR to go in first so that I don't cause conflicts, but if it helps I can get that change out right now.

Hm, ok, in the meantime I guess I'll leave this and then we can rebase your change onto it? Unsure what the best intermediate change would be.


pkg/sql/opt/optbuilder/mutation_builder.go, line 932 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

[nit] i is shadowing the outer variable

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 948 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

it's a bit odd to return "abort" as a bool -- seems more idiomatic to return "ok" and check for "!ok"

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 966 at r1 (raw file):

Previously, RaduBerinde wrote…

[nit] could be more clear, eg "Each row input to the Update operator contains both the existing and the new value for each updated column"

Yeah that's much better! Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 974 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

each the -> each of the

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1022 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

can use numCols here

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1024 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

[nit] shadowing outer variable

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1025 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

you could save this in a variable to avoid calling it twice

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1034 at r1 (raw file):

Previously, RaduBerinde wrote…

Maybe mention that this could also be ExceptAll, it's not 100% clear to me which would be better in practice.

Interesting point. I guess in general if we were excepting on a key in some circumstances we could normalize ExceptAll to Except, (or vice versa) if we decided that one was preferable to the other.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1053 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

[nit] shadowing

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1075 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

What input? I just see fkOrdinal

Meant to imply that the input to the check here is the same as the input to the mutation operator, tried to make that clearer.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1076 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

a list of?

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1083 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

[nit] this can be i instead of j

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1216 at r1 (raw file):

Previously, RaduBerinde wrote…

[nit] a comment would help

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1286 at r1 (raw file):

Previously, RaduBerinde wrote…

This definitely needs a comment

Done.


pkg/sql/opt/optbuilder/testdata/fk-checks-update, line 73 at r1 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

Is this check necessary since we're not updating c?

Good call, not necessary! I added some code to not add checks when no relevant columns are updated.


pkg/sql/opt/optbuilder/testdata/fk-checks-update, line 232 at r1 (raw file):

Previously, RaduBerinde wrote…

We need more tests here from a bunch of angles:

  • cases like the one above where we don't want to check FKs that aren't being modified

  • multi-column FKs, where only some of the columns are updated, including cases where the columns are in different families. By the way, we have an interesting TODO here: I can't find any code that figures out that if you modify column a and you have an outgoing FK on (a,b) you also need to scan b; I believe this currently happens automatically because we always have an index on a,b, but when we remove that requirement this won't be the case anymore. This can wait until 19.2 but it should be spelled out somewhere.

  • a self-referencing FK

That's a very interesting case indeed! Added a couple cases that should break when that changes and added a comment.

Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @justinj, @RaduBerinde, and @rytaft)


pkg/sql/opt/ops/mutation.opt, line 227 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

Hm, ok, in the meantime I guess I'll leave this and then we can rebase your change onto it? Unsure what the best intermediate change would be.

You could make this field a string in this change (basically move up the case that converts the op to "insert", "delete", or whatever). Either way is fine, I'll rebase my change after yours and figure it out.

Copy link
Collaborator

@rytaft rytaft left a comment

Choose a reason for hiding this comment

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

:lgtm: awesome!

Reviewed 14 of 14 files at r2.
Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (waiting on @justinj and @RaduBerinde)


pkg/sql/logictest/testdata/logic_test/fk_opt, line 2796 at r2 (raw file):

# This case illustrates why it's insufficient to just check the "old" values,
# even in the absence of temporary unique constraint violations. In this case,
# the "new" and "old" values are the same, so we have to remove the new values

[nit] remove -> access?


pkg/sql/opt/optbuilder/mutation_builder.go, line 1231 at r2 (raw file):

}

// addDeletionCheck adds a FK check for rows which are removed from a tabel.

tabel -> table

Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

:lgtm: Nice work!

Reviewable status: :shipit: complete! 2 of 0 LGTMs obtained (waiting on @justinj)

@justinj justinj force-pushed the fk-update branch 3 times, most recently from 13ed8f4 to d0d49ba Compare August 27, 2019 14:32
Copy link
Contributor Author

@justinj justinj left a comment

Choose a reason for hiding this comment

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

Spent a while trying to understand what was going wrong with the test failure, turned out that disabling FK checks for cascading actions only refers to outbound references, and inbound ones still need to be checked. Turns out to be a simple fix and I just needed to remove one of the ifs I added. There's probably a cleaner way to fit this in but hopefully this code path will be simplified further by the removal of old-style FK checks in the near future. Would appreciate a quick look!

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (and 2 stale) (waiting on @rytaft)


pkg/sql/logictest/testdata/logic_test/fk_opt, line 2796 at r2 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

[nit] remove -> access?

tried to make this clearer


pkg/sql/opt/ops/mutation.opt, line 227 at r1 (raw file):

Previously, RaduBerinde wrote…

You could make this field a string in this change (basically move up the case that converts the op to "insert", "delete", or whatever). Either way is fine, I'll rebase my change after yours and figure it out.

Done.


pkg/sql/opt/optbuilder/mutation_builder.go, line 1231 at r2 (raw file):

Previously, rytaft (Rebecca Taft) wrote…

tabel -> table

Done.

Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

LGTM (though I'm not particularly familiar with the row/ code)

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (and 2 stale) (waiting on @rytaft)

@justinj
Copy link
Contributor Author

justinj commented Aug 27, 2019

Thanks!

bors r+

Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

Probably need to rebase on 40214 - will be cool to see the updates to the plans.

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (and 2 stale) (waiting on @rytaft)

@craig
Copy link
Contributor

craig bot commented Aug 27, 2019

Build failed (retrying...)

@craig
Copy link
Contributor

craig bot commented Aug 27, 2019

Build failed (retrying...)

@justinj
Copy link
Contributor Author

justinj commented Aug 27, 2019

bors r-

@craig
Copy link
Contributor

craig bot commented Aug 27, 2019

Canceled

@justinj
Copy link
Contributor Author

justinj commented Aug 27, 2019

Turns out none of the updates require FK checks, the test failure was because we were still buffering the input if we ended up without any checks, which isn't a problem for correctness, but it's a little nicer if we don't, so I've made a change to not buffer up if we end up without checks.

bors r+

craig bot pushed a commit that referenced this pull request Aug 27, 2019
39751: opt: add FK checks for UPDATE statements r=justinj a=justinj

Been trying to get this in a reviewable state for a while—still not quite perfect but gotta get it out the door, I think. Going to have to rebase when #39462 goes in to get some better testing coverage.

Release note: None

Co-authored-by: Justin Jaffray <justin@cockroachlabs.com>
Copy link
Member

@RaduBerinde RaduBerinde left a comment

Choose a reason for hiding this comment

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

:lgtm:

Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (and 1 stale) (waiting on @rytaft)

@craig
Copy link
Contributor

craig bot commented Aug 27, 2019

Build succeeded

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