github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/opt/optbuilder/mutation_builder_fk.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package optbuilder
    12  
    13  import (
    14  	"github.com/cockroachdb/cockroach/pkg/server/telemetry"
    15  	"github.com/cockroachdb/cockroach/pkg/sql/opt"
    16  	"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
    17  	"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/privilege"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/sqltelemetry"
    21  	"github.com/cockroachdb/errors"
    22  )
    23  
    24  // This file contains methods that populate mutationBuilder.checks and cascades.
    25  //
    26  // -- Checks --
    27  //
    28  // The foreign key checks are queries that run after the statement (including
    29  // the relevant mutation) completes. They check the integrity of the foreign key
    30  // relations that involve modified rows; any row that is returned by these FK
    31  // check queries indicates a foreign key violation.
    32  //
    33  // -- Cacades --
    34  //
    35  // The foreign key cascades are "potential" future queries that perform
    36  // cascading mutations of child tables. These queries are constructed later as
    37  // necessary; mb.cascades stores metadata that include CascadeBuilder instances
    38  // which are used to construct these queries.
    39  
    40  // buildFKChecksForInsert builds FK check queries for an insert.
    41  //
    42  // See the comment at the top of the file for general information on checks and
    43  // cascades.
    44  //
    45  // In the case of insert, each FK check query is an anti-join with the left side
    46  // being a WithScan of the mutation input and the right side being the
    47  // referenced table. A simple example of an insert with a FK check:
    48  //
    49  //   insert child
    50  //    ├── ...
    51  //    ├── input binding: &1
    52  //    └── f-k-checks
    53  //         └── f-k-checks-item: child(p) -> parent(p)
    54  //              └── anti-join (hash)
    55  //                   ├── columns: column2:5!null
    56  //                   ├── with-scan &1
    57  //                   │    ├── columns: column2:5!null
    58  //                   │    └── mapping:
    59  //                   │         └──  column2:4 => column2:5
    60  //                   ├── scan parent
    61  //                   │    └── columns: parent.p:6!null
    62  //                   └── filters
    63  //                        └── column2:5 = parent.p:6
    64  //
    65  // See testdata/fk-checks-insert for more examples.
    66  func (mb *mutationBuilder) buildFKChecksForInsert() {
    67  	if mb.tab.OutboundForeignKeyCount() == 0 {
    68  		// No relevant FKs.
    69  		return
    70  	}
    71  	if !mb.b.evalCtx.SessionData.OptimizerFKChecks {
    72  		mb.setFKFallback()
    73  		return
    74  	}
    75  
    76  	// TODO(radu): if the input is a VALUES with constant expressions, we don't
    77  	// need to buffer it. This could be a normalization rule, but it's probably
    78  	// more efficient if we did it in here (or we'd end up building the entire FK
    79  	// subtrees twice).
    80  	mb.withID = mb.b.factory.Memo().NextWithID()
    81  
    82  	h := &mb.fkCheckHelper
    83  	for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ {
    84  		if h.initWithOutboundFK(mb, i) {
    85  			mb.checks = append(mb.checks, h.buildInsertionCheck())
    86  		}
    87  	}
    88  	telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter)
    89  }
    90  
    91  // buildFKChecksAndCascadesForDelete builds FK check and cascades for a delete.
    92  //
    93  // See the comment at the top of the file for general information on checks and
    94  // cascades.
    95  //
    96  // -- Checks --
    97  //
    98  // In the case of delete, each FK check query is a semi-join with the left side
    99  // being a WithScan of the mutation input and the right side being the
   100  // referencing table. For example:
   101  //   delete parent
   102  //    ├── ...
   103  //    ├── input binding: &1
   104  //    └── f-k-checks
   105  //         └── f-k-checks-item: child(p) -> parent(p)
   106  //              └── semi-join (hash)
   107  //                   ├── columns: p:7!null
   108  //                   ├── with-scan &1
   109  //                   │    ├── columns: p:7!null
   110  //                   │    └── mapping:
   111  //                   │         └──  parent.p:5 => p:7
   112  //                   ├── scan child
   113  //                   │    └── columns: child.p:9!null
   114  //                   └── filters
   115  //                        └── p:7 = child.p:9
   116  //
   117  // See testdata/fk-checks-delete for more examples.
   118  //
   119  // -- Cascades --
   120  //
   121  // See onDeleteCascadeBuilder, onDeleteSetBuilder for details.
   122  //
   123  func (mb *mutationBuilder) buildFKChecksAndCascadesForDelete() {
   124  	if mb.tab.InboundForeignKeyCount() == 0 {
   125  		// No relevant FKs.
   126  		return
   127  	}
   128  	if !mb.b.evalCtx.SessionData.OptimizerFKChecks {
   129  		mb.setFKFallback()
   130  		return
   131  	}
   132  
   133  	mb.withID = mb.b.factory.Memo().NextWithID()
   134  
   135  	for i, n := 0, mb.tab.InboundForeignKeyCount(); i < n; i++ {
   136  		h := &mb.fkCheckHelper
   137  		if !h.initWithInboundFK(mb, i) {
   138  			continue
   139  		}
   140  		// The action dictates how a foreign key reference is handled:
   141  		//  - with Cascade/SetNull/SetDefault, we create a cascading mutation to
   142  		//    modify or delete "orphaned" rows in the child table.
   143  		//  - with Restrict/NoAction, we create a check that causes an error if
   144  		//    there are any "orhpaned" rows in the child table.
   145  		if a := h.fk.DeleteReferenceAction(); a != tree.Restrict && a != tree.NoAction {
   146  			if !mb.b.evalCtx.SessionData.OptimizerFKCascades {
   147  				// Bail, so that exec FK checks pick up on FK checks and perform them.
   148  				mb.setFKFallback()
   149  				telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter)
   150  				return
   151  			}
   152  
   153  			var builder memo.CascadeBuilder
   154  			switch a {
   155  			case tree.Cascade:
   156  				builder = newOnDeleteCascadeBuilder(mb.tab, i, h.otherTab)
   157  			case tree.SetNull, tree.SetDefault:
   158  				builder = newOnDeleteSetBuilder(mb.tab, i, h.otherTab, a)
   159  			default:
   160  				panic(errors.AssertionFailedf("unhandled action type %s", a))
   161  			}
   162  
   163  			cols := make(opt.ColList, len(h.tabOrdinals))
   164  			for i, tabOrd := range h.tabOrdinals {
   165  				cols[i] = mb.scopeOrdToColID(mb.fetchOrds[tabOrd])
   166  			}
   167  			mb.cascades = append(mb.cascades, memo.FKCascade{
   168  				FKName:    h.fk.Name(),
   169  				Builder:   builder,
   170  				WithID:    mb.withID,
   171  				OldValues: cols,
   172  				NewValues: nil,
   173  			})
   174  			continue
   175  		}
   176  
   177  		fkInput, withScanCols, _ := h.makeFKInputScan(fkInputScanFetchedVals)
   178  		mb.checks = append(mb.checks, h.buildDeletionCheck(fkInput, withScanCols))
   179  	}
   180  	telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter)
   181  }
   182  
   183  // buildFKChecksForUpdate builds FK check queries for an update.
   184  //
   185  // See the comment at the top of the file for general information on checks and
   186  // cascades.
   187  //
   188  // In the case of update, there are two types of FK check queries:
   189  //
   190  //  - insertion-side checks are very similar to the checks we issue for insert;
   191  //    they are an anti-join with the left side being a WithScan of the "new"
   192  //    values for each row. For example:
   193  //      update child
   194  //       ├── ...
   195  //       ├── input binding: &1
   196  //       └── f-k-checks
   197  //            └── f-k-checks-item: child(p) -> parent(p)
   198  //                 └── anti-join (hash)
   199  //                      ├── columns: column5:6!null
   200  //                      ├── with-scan &1
   201  //                      │    ├── columns: column5:6!null
   202  //                      │    └── mapping:
   203  //                      │         └──  column5:5 => column5:6
   204  //                      ├── scan parent
   205  //                      │    └── columns: parent.p:8!null
   206  //                      └── filters
   207  //                           └── column5:6 = parent.p:8
   208  //
   209  //  - deletion-side checks are similar to the checks we issue for delete; they
   210  //    are a semi-join but the left side input is more complicated: it is an
   211  //    Except between a WithScan of the "old" values and a WithScan of the "new"
   212  //    values for each row (this is the set of values that are effectively
   213  //    removed from the table). For example:
   214  //      update parent
   215  //       ├── ...
   216  //       ├── input binding: &1
   217  //       └── f-k-checks
   218  //            └── f-k-checks-item: child(p) -> parent(p)
   219  //                 └── semi-join (hash)
   220  //                      ├── columns: p:8!null
   221  //                      ├── except
   222  //                      │    ├── columns: p:8!null
   223  //                      │    ├── left columns: p:8!null
   224  //                      │    ├── right columns: column7:9
   225  //                      │    ├── with-scan &1
   226  //                      │    │    ├── columns: p:8!null
   227  //                      │    │    └── mapping:
   228  //                      │    │         └──  parent.p:5 => p:8
   229  //                      │    └── with-scan &1
   230  //                      │         ├── columns: column7:9!null
   231  //                      │         └── mapping:
   232  //                      │              └──  column7:7 => column7:9
   233  //                      ├── scan child
   234  //                      │    └── columns: child.p:11!null
   235  //                      └── filters
   236  //                           └── p:8 = child.p:11
   237  //
   238  // Only FK relations that involve updated columns result in FK checks.
   239  //
   240  func (mb *mutationBuilder) buildFKChecksForUpdate() {
   241  	if mb.tab.OutboundForeignKeyCount() == 0 && mb.tab.InboundForeignKeyCount() == 0 {
   242  		return
   243  	}
   244  	if !mb.b.evalCtx.SessionData.OptimizerFKChecks {
   245  		mb.setFKFallback()
   246  		return
   247  	}
   248  
   249  	mb.withID = mb.b.factory.Memo().NextWithID()
   250  
   251  	// An Update can be thought of an insertion paired with a deletion, so for an
   252  	// Update we can emit both semi-joins and anti-joins.
   253  
   254  	// Each row input to the Update operator contains both the existing and the
   255  	// new value for each updated column. From this we can construct the effective
   256  	// insertion and deletion.
   257  
   258  	// Say the table being updated by an update is:
   259  	//
   260  	//   x | y | z
   261  	//   --+---+--
   262  	//   1 | 3 | 5
   263  	//
   264  	// And we are executing UPDATE t SET y = 10, then the input to the Update
   265  	// operator will look like:
   266  	//
   267  	//   x | y | z | new_y
   268  	//   --+---+---+------
   269  	//   1 | 3 | 5 |  10
   270  	//
   271  	// The insertion check will happen on the "new" row (x, new_y, z); the deletion
   272  	// check will happen on the "old" row (x, y, z).
   273  
   274  	h := &mb.fkCheckHelper
   275  	for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ {
   276  		// Verify that at least one FK column is actually updated.
   277  		if mb.outboundFKColsUpdated(i) {
   278  			if h.initWithOutboundFK(mb, i) {
   279  				mb.checks = append(mb.checks, h.buildInsertionCheck())
   280  			}
   281  		}
   282  	}
   283  
   284  	// The "deletion" incurred by an update is the rows deleted for a given
   285  	// inbound FK minus the rows inserted.
   286  	for i, n := 0, mb.tab.InboundForeignKeyCount(); i < n; i++ {
   287  		// Verify that at least one FK column is actually updated.
   288  		if !mb.inboundFKColsUpdated(i) {
   289  			continue
   290  		}
   291  		if !h.initWithInboundFK(mb, i) {
   292  			// The FK constraint can safely be ignored.
   293  			continue
   294  		}
   295  
   296  		if a := h.fk.UpdateReferenceAction(); a != tree.Restrict && a != tree.NoAction {
   297  			if !mb.b.evalCtx.SessionData.OptimizerFKCascades {
   298  				// Bail, so that exec FK checks pick up on FK checks and perform them.
   299  				mb.setFKFallback()
   300  				telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter)
   301  				return
   302  			}
   303  			builder := newOnUpdateCascadeBuilder(mb.tab, i, h.otherTab, a)
   304  
   305  			oldCols := make(opt.ColList, len(h.tabOrdinals))
   306  			newCols := make(opt.ColList, len(h.tabOrdinals))
   307  			for i, tabOrd := range h.tabOrdinals {
   308  				fetchOrd := mb.fetchOrds[tabOrd]
   309  				updateOrd := mb.updateOrds[tabOrd]
   310  				if updateOrd == -1 {
   311  					updateOrd = fetchOrd
   312  				}
   313  
   314  				oldCols[i] = mb.scopeOrdToColID(fetchOrd)
   315  				newCols[i] = mb.scopeOrdToColID(updateOrd)
   316  			}
   317  			mb.cascades = append(mb.cascades, memo.FKCascade{
   318  				FKName:    h.fk.Name(),
   319  				Builder:   builder,
   320  				WithID:    mb.withID,
   321  				OldValues: oldCols,
   322  				NewValues: newCols,
   323  			})
   324  			continue
   325  		}
   326  
   327  		// Construct an Except expression for the set difference between "old"
   328  		// FK values and "new" FK values.
   329  		//
   330  		// The simplest example to see why this is necessary is when we are
   331  		// "updating" a value to the same value, e.g:
   332  		//   UPDATE child SET c = c
   333  		// Here we are not removing any values from the column, so we must not
   334  		// check for orphaned rows or we will be generating bogus FK violation
   335  		// errors.
   336  		//
   337  		// There are more complicated cases where one row replaces the value from
   338  		// another row, e.g.
   339  		//   UPDATE child SET c = c+1
   340  		// when we have existing consecutive values. These cases are sketchy because
   341  		// depending on the order in which the mutations are applied, they may or
   342  		// may not result in unique index violations (but if they go through, the FK
   343  		// checks should be accurate).
   344  		//
   345  		// Note that the same reasoning could be applied to the insertion checks,
   346  		// but in that case, it is not a correctness issue: it's always ok to
   347  		// recheck that an existing row is not orphan. It's not really desirable for
   348  		// performance either: we would be incurring extra cost (more complicated
   349  		// expressions, scanning the input buffer twice) for a rare case.
   350  
   351  		oldRows, colsForOldRow, _ := h.makeFKInputScan(fkInputScanFetchedVals)
   352  		newRows, colsForNewRow, _ := h.makeFKInputScan(fkInputScanNewVals)
   353  
   354  		// The rows that no longer exist are the ones that were "deleted" by virtue
   355  		// of being updated _from_, minus the ones that were "added" by virtue of
   356  		// being updated _to_.
   357  		deletedRows := mb.b.factory.ConstructExcept(
   358  			oldRows,
   359  			newRows,
   360  			&memo.SetPrivate{
   361  				LeftCols:  colsForOldRow,
   362  				RightCols: colsForNewRow,
   363  				OutCols:   colsForOldRow,
   364  			},
   365  		)
   366  
   367  		mb.checks = append(mb.checks, h.buildDeletionCheck(deletedRows, colsForOldRow))
   368  	}
   369  	telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter)
   370  }
   371  
   372  // buildFKChecksForUpsert builds FK check queries for an upsert.
   373  //
   374  // See the comment at the top of the file for general information on checks and
   375  // cascades.
   376  //
   377  // The case of upsert is very similar to update; see buildFKChecksForUpdate.
   378  // The main difference is that for update, the "new" values were readily
   379  // available, whereas for upsert, the "new" values can be the result of an
   380  // expression of the form:
   381  //   CASE WHEN canary IS NULL THEN inserter-value ELSE updated-value END
   382  // These expressions are already projected as part of the mutation input and are
   383  // directly accessible through WithScan.
   384  //
   385  // Only FK relations that involve updated columns result in deletion-side FK
   386  // checks. The insertion-side FK checks are always needed (similar to insert)
   387  // because any of the rows might result in an insert rather than an update.
   388  //
   389  func (mb *mutationBuilder) buildFKChecksForUpsert() {
   390  	numOutbound := mb.tab.OutboundForeignKeyCount()
   391  	numInbound := mb.tab.InboundForeignKeyCount()
   392  
   393  	if numOutbound == 0 && numInbound == 0 {
   394  		return
   395  	}
   396  
   397  	if !mb.b.evalCtx.SessionData.OptimizerFKChecks {
   398  		mb.setFKFallback()
   399  		return
   400  	}
   401  
   402  	mb.withID = mb.b.factory.Memo().NextWithID()
   403  
   404  	h := &mb.fkCheckHelper
   405  	for i := 0; i < numOutbound; i++ {
   406  		if h.initWithOutboundFK(mb, i) {
   407  			mb.checks = append(mb.checks, h.buildInsertionCheck())
   408  		}
   409  	}
   410  
   411  	for i := 0; i < numInbound; i++ {
   412  		// Verify that at least one FK column is updated by the Upsert; columns that
   413  		// are not updated can get new values (through the insert path) but existing
   414  		// values are never removed.
   415  		if !mb.inboundFKColsUpdated(i) {
   416  			continue
   417  		}
   418  
   419  		if !h.initWithInboundFK(mb, i) {
   420  			continue
   421  		}
   422  
   423  		if a := h.fk.UpdateReferenceAction(); a != tree.Restrict && a != tree.NoAction {
   424  			// Bail, so that exec FK checks pick up on FK checks and perform them.
   425  			mb.setFKFallback()
   426  			telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter)
   427  			return
   428  		}
   429  
   430  		// Construct an Except expression for the set difference between "old" FK
   431  		// values and "new" FK values. See buildFKChecksForUpdate for more details.
   432  		//
   433  		// Note that technically, to get "old" values for the updated rows we should
   434  		// be selecting only the rows that correspond to updates, as opposed to
   435  		// insertions (using a "canaryCol IS NOT NULL" condition). But the rows we
   436  		// would filter out have all-null fetched values anyway and will never match
   437  		// in the semi join.
   438  		oldRows, colsForOldRow, _ := h.makeFKInputScan(fkInputScanFetchedVals)
   439  		newRows, colsForNewRow, _ := h.makeFKInputScan(fkInputScanNewVals)
   440  
   441  		// The rows that no longer exist are the ones that were "deleted" by virtue
   442  		// of being updated _from_, minus the ones that were "added" by virtue of
   443  		// being updated _to_.
   444  		deletedRows := mb.b.factory.ConstructExcept(
   445  			oldRows,
   446  			newRows,
   447  			&memo.SetPrivate{
   448  				LeftCols:  colsForOldRow,
   449  				RightCols: colsForNewRow,
   450  				OutCols:   colsForOldRow,
   451  			},
   452  		)
   453  		mb.checks = append(mb.checks, h.buildDeletionCheck(deletedRows, colsForOldRow))
   454  	}
   455  	telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter)
   456  }
   457  
   458  // outboundFKColsUpdated returns true if any of the FK columns for an outbound
   459  // constraint are being updated (according to updateOrds).
   460  func (mb *mutationBuilder) outboundFKColsUpdated(fkOrdinal int) bool {
   461  	fk := mb.tab.OutboundForeignKey(fkOrdinal)
   462  	for i, n := 0, fk.ColumnCount(); i < n; i++ {
   463  		if ord := fk.OriginColumnOrdinal(mb.tab, i); mb.updateOrds[ord] != -1 {
   464  			return true
   465  		}
   466  	}
   467  	return false
   468  }
   469  
   470  // inboundFKColsUpdated returns true if any of the FK columns for an inbound
   471  // constraint are being updated (according to updateOrds).
   472  func (mb *mutationBuilder) inboundFKColsUpdated(fkOrdinal int) bool {
   473  	fk := mb.tab.InboundForeignKey(fkOrdinal)
   474  	for i, n := 0, fk.ColumnCount(); i < n; i++ {
   475  		if ord := fk.ReferencedColumnOrdinal(mb.tab, i); mb.updateOrds[ord] != -1 {
   476  			return true
   477  		}
   478  	}
   479  	return false
   480  }
   481  
   482  // fkCheckHelper is a type associated with a single FK constraint and is used to
   483  // build the "leaves" of a FK check expression, namely the WithScan of the
   484  // mutation input and the Scan of the other table.
   485  type fkCheckHelper struct {
   486  	mb *mutationBuilder
   487  
   488  	fk         cat.ForeignKeyConstraint
   489  	fkOrdinal  int
   490  	fkOutbound bool
   491  
   492  	otherTab cat.Table
   493  
   494  	// tabOrdinals are the table ordinals of the FK columns in the table that is
   495  	// being mutated. They correspond 1-to-1 to the columns in the
   496  	// ForeignKeyConstraint.
   497  	tabOrdinals []int
   498  	// otherTabOrdinals are the table ordinals of the FK columns in the "other"
   499  	// table. They correspond 1-to-1 to the columns in the ForeignKeyConstraint.
   500  	otherTabOrdinals []int
   501  }
   502  
   503  // initWithOutboundFK initializes the helper with an outbound FK constraint.
   504  //
   505  // Returns false if the FK relation should be ignored (e.g. because the new
   506  // values for the FK columns are known to be always NULL).
   507  func (h *fkCheckHelper) initWithOutboundFK(mb *mutationBuilder, fkOrdinal int) bool {
   508  	*h = fkCheckHelper{
   509  		mb:         mb,
   510  		fk:         mb.tab.OutboundForeignKey(fkOrdinal),
   511  		fkOrdinal:  fkOrdinal,
   512  		fkOutbound: true,
   513  	}
   514  
   515  	refID := h.fk.ReferencedTableID()
   516  	ref, isAdding, err := mb.b.catalog.ResolveDataSourceByID(mb.b.ctx, cat.Flags{}, refID)
   517  	if err != nil {
   518  		if isAdding {
   519  			// The other table is in the process of being added; ignore the FK relation.
   520  			return false
   521  		}
   522  		panic(err)
   523  	}
   524  	// We need SELECT privileges on the referenced table.
   525  	mb.b.checkPrivilege(opt.DepByID(refID), ref, privilege.SELECT)
   526  	h.otherTab = ref.(cat.Table)
   527  
   528  	numCols := h.fk.ColumnCount()
   529  	h.allocOrdinals(numCols)
   530  	for i := 0; i < numCols; i++ {
   531  		h.tabOrdinals[i] = h.fk.OriginColumnOrdinal(mb.tab, i)
   532  		h.otherTabOrdinals[i] = h.fk.ReferencedColumnOrdinal(h.otherTab, i)
   533  	}
   534  
   535  	// Check if we are setting NULL values for the FK columns, like when this
   536  	// mutation is the result of a SET NULL cascade action.
   537  	numNullCols := 0
   538  	for _, tabOrd := range h.tabOrdinals {
   539  		col := mb.scopeOrdToColID(mb.mapToReturnScopeOrd(tabOrd))
   540  		if memo.OutputColumnIsAlwaysNull(mb.outScope.expr, col) {
   541  			numNullCols++
   542  		}
   543  	}
   544  	if numNullCols == numCols {
   545  		// All FK columns are getting NULL values; FK check not needed.
   546  		return false
   547  	}
   548  	if numNullCols > 0 && h.fk.MatchMethod() == tree.MatchSimple {
   549  		// At least one FK column is getting a NULL value and we are using MATCH
   550  		// SIMPLE; FK check not needed.
   551  		return false
   552  	}
   553  
   554  	return true
   555  }
   556  
   557  // initWithInboundFK initializes the helper with an inbound FK constraint.
   558  //
   559  // Returns false if the FK relation should be ignored (because the other table
   560  // is in the process of being created).
   561  func (h *fkCheckHelper) initWithInboundFK(mb *mutationBuilder, fkOrdinal int) (ok bool) {
   562  	*h = fkCheckHelper{
   563  		mb:         mb,
   564  		fk:         mb.tab.InboundForeignKey(fkOrdinal),
   565  		fkOrdinal:  fkOrdinal,
   566  		fkOutbound: false,
   567  	}
   568  
   569  	originID := h.fk.OriginTableID()
   570  	ref, isAdding, err := mb.b.catalog.ResolveDataSourceByID(mb.b.ctx, cat.Flags{}, originID)
   571  	if err != nil {
   572  		if isAdding {
   573  			// The other table is in the process of being added; ignore the FK relation.
   574  			return false
   575  		}
   576  		panic(err)
   577  	}
   578  	// We need SELECT privileges on the origin table.
   579  	mb.b.checkPrivilege(opt.DepByID(originID), ref, privilege.SELECT)
   580  	h.otherTab = ref.(cat.Table)
   581  
   582  	numCols := h.fk.ColumnCount()
   583  	h.allocOrdinals(numCols)
   584  	for i := 0; i < numCols; i++ {
   585  		h.tabOrdinals[i] = h.fk.ReferencedColumnOrdinal(mb.tab, i)
   586  		h.otherTabOrdinals[i] = h.fk.OriginColumnOrdinal(h.otherTab, i)
   587  	}
   588  
   589  	return true
   590  }
   591  
   592  type fkInputScanType uint8
   593  
   594  const (
   595  	fkInputScanNewVals fkInputScanType = iota
   596  	fkInputScanFetchedVals
   597  )
   598  
   599  // makeFKInputScan constructs a WithScan that iterates over the input to the
   600  // mutation operator. Used in expressions that generate rows for checking for FK
   601  // violations.
   602  //
   603  // The WithScan expression will scan either the new values or the fetched values
   604  // for the given table ordinals (which correspond to FK columns).
   605  //
   606  // Returns the output columns from the WithScan, which map 1-to-1 to
   607  // h.tabOrdinals. Also returns the subset of these columns that can be assumed
   608  // to be not null (either because they are not null in the mutation input or
   609  // because they are non-nullable table columns).
   610  //
   611  func (h *fkCheckHelper) makeFKInputScan(
   612  	typ fkInputScanType,
   613  ) (scan memo.RelExpr, outCols opt.ColList, notNullOutCols opt.ColSet) {
   614  	mb := h.mb
   615  	// inputCols are the column IDs from the mutation input that we are scanning.
   616  	inputCols := make(opt.ColList, len(h.tabOrdinals))
   617  	// outCols will store the newly synthesized output columns for WithScan.
   618  	outCols = make(opt.ColList, len(inputCols))
   619  	for i, tabOrd := range h.tabOrdinals {
   620  		if typ == fkInputScanNewVals {
   621  			inputCols[i] = mb.scopeOrdToColID(mb.mapToReturnScopeOrd(tabOrd))
   622  		} else {
   623  			inputCols[i] = mb.scopeOrdToColID(mb.fetchOrds[tabOrd])
   624  		}
   625  		if inputCols[i] == 0 {
   626  			panic(errors.AssertionFailedf("no value for FK column (tabOrd=%d)", tabOrd))
   627  		}
   628  
   629  		// Synthesize new column.
   630  		c := mb.b.factory.Metadata().ColumnMeta(inputCols[i])
   631  		outCols[i] = mb.md.AddColumn(c.Alias, c.Type)
   632  
   633  		// If a table column is not nullable, NULLs cannot be inserted (the
   634  		// mutation will fail). So for the purposes of FK checks, we can treat
   635  		// these columns as not null.
   636  		if mb.outScope.expr.Relational().NotNullCols.Contains(inputCols[i]) ||
   637  			!mb.tab.Column(tabOrd).IsNullable() {
   638  			notNullOutCols.Add(outCols[i])
   639  		}
   640  	}
   641  
   642  	scan = mb.b.factory.ConstructWithScan(&memo.WithScanPrivate{
   643  		With:         mb.withID,
   644  		InCols:       inputCols,
   645  		OutCols:      outCols,
   646  		BindingProps: mb.outScope.expr.Relational(),
   647  		ID:           mb.b.factory.Metadata().NextUniqueID(),
   648  	})
   649  	return scan, outCols, notNullOutCols
   650  }
   651  
   652  // buildOtherTableScan builds a Scan of the "other" table.
   653  func (h *fkCheckHelper) buildOtherTableScan() (outScope *scope, tabMeta *opt.TableMeta) {
   654  	otherTabMeta := h.mb.b.addTable(h.otherTab, tree.NewUnqualifiedTableName(h.otherTab.Name()))
   655  	return h.mb.b.buildScan(
   656  		otherTabMeta,
   657  		h.otherTabOrdinals,
   658  		&tree.IndexFlags{IgnoreForeignKeys: true},
   659  		noRowLocking,
   660  		excludeMutations,
   661  		h.mb.b.allocScope(),
   662  	), otherTabMeta
   663  }
   664  
   665  func (h *fkCheckHelper) allocOrdinals(numCols int) {
   666  	buf := make([]int, numCols*2)
   667  	h.tabOrdinals = buf[:numCols]
   668  	h.otherTabOrdinals = buf[numCols:]
   669  }
   670  
   671  // buildInsertionCheck creates a FK check for rows which are added to a table.
   672  // The input to the insertion check will be produced from the input to the
   673  // mutation operator.
   674  func (h *fkCheckHelper) buildInsertionCheck() memo.FKChecksItem {
   675  	fkInput, withScanCols, notNullWithScanCols := h.makeFKInputScan(fkInputScanNewVals)
   676  
   677  	numCols := len(withScanCols)
   678  	f := h.mb.b.factory
   679  	if notNullWithScanCols.Len() < numCols {
   680  		// The columns we are inserting might have NULLs. These require special
   681  		// handling, depending on the match method:
   682  		//  - MATCH SIMPLE: allows any column(s) to be NULL and the row doesn't
   683  		//                  need to have a match in the referenced table.
   684  		//  - MATCH FULL: only the case where *all* the columns are NULL is
   685  		//                allowed, and the row doesn't need to have a match in the
   686  		//                referenced table.
   687  		//
   688  		// Note that rows that have NULLs will never have a match in the anti
   689  		// join and will generate errors. To handle these cases, we filter the
   690  		// mutated rows (before the anti join) to remove those which don't need a
   691  		// match.
   692  		//
   693  		// For SIMPLE, we filter out any rows which have a NULL. For FULL, we
   694  		// filter out any rows where all the columns are NULL (rows which have
   695  		// NULLs a subset of columns are let through and will generate FK errors
   696  		// because they will never have a match in the anti join).
   697  		switch m := h.fk.MatchMethod(); m {
   698  		case tree.MatchSimple:
   699  			// Filter out any rows which have a NULL; build filters of the form
   700  			//   (a IS NOT NULL) AND (b IS NOT NULL) ...
   701  			filters := make(memo.FiltersExpr, 0, numCols-notNullWithScanCols.Len())
   702  			for _, col := range withScanCols {
   703  				if !notNullWithScanCols.Contains(col) {
   704  					filters = append(filters, f.ConstructFiltersItem(
   705  						f.ConstructIsNot(
   706  							f.ConstructVariable(col),
   707  							memo.NullSingleton,
   708  						),
   709  					))
   710  				}
   711  			}
   712  			fkInput = f.ConstructSelect(fkInput, filters)
   713  
   714  		case tree.MatchFull:
   715  			// Filter out any rows which have NULLs on all referencing columns.
   716  			if !notNullWithScanCols.Empty() {
   717  				// We statically know that some of the referencing columns can't be
   718  				// NULL. In this case, we don't need to filter anything (the case
   719  				// where all the origin columns are NULL is not possible).
   720  				break
   721  			}
   722  			// Build a filter of the form
   723  			//   (a IS NOT NULL) OR (b IS NOT NULL) ...
   724  			var condition opt.ScalarExpr
   725  			for _, col := range withScanCols {
   726  				is := f.ConstructIsNot(
   727  					f.ConstructVariable(col),
   728  					memo.NullSingleton,
   729  				)
   730  				if condition == nil {
   731  					condition = is
   732  				} else {
   733  					condition = f.ConstructOr(condition, is)
   734  				}
   735  			}
   736  			fkInput = f.ConstructSelect(
   737  				fkInput,
   738  				memo.FiltersExpr{f.ConstructFiltersItem(condition)},
   739  			)
   740  
   741  		default:
   742  			panic(errors.AssertionFailedf("match method %s not supported", m))
   743  		}
   744  	}
   745  
   746  	// Build an anti-join, with the origin FK columns on the left and the
   747  	// referenced columns on the right.
   748  
   749  	scanScope, refTabMeta := h.buildOtherTableScan()
   750  
   751  	// Build the join filters:
   752  	//   (origin_a = referenced_a) AND (origin_b = referenced_b) AND ...
   753  	antiJoinFilters := make(memo.FiltersExpr, numCols)
   754  	for j := 0; j < numCols; j++ {
   755  		antiJoinFilters[j] = f.ConstructFiltersItem(
   756  			f.ConstructEq(
   757  				f.ConstructVariable(withScanCols[j]),
   758  				f.ConstructVariable(scanScope.cols[j].id),
   759  			),
   760  		)
   761  	}
   762  	antiJoin := f.ConstructAntiJoin(
   763  		fkInput, scanScope.expr, antiJoinFilters, &memo.JoinPrivate{},
   764  	)
   765  
   766  	return f.ConstructFKChecksItem(antiJoin, &memo.FKChecksItemPrivate{
   767  		OriginTable:     h.mb.tabID,
   768  		ReferencedTable: refTabMeta.MetaID,
   769  		FKOutbound:      true,
   770  		FKOrdinal:       h.fkOrdinal,
   771  		KeyCols:         withScanCols,
   772  		OpName:          h.mb.opName,
   773  	})
   774  }
   775  
   776  // buildDeletionCheck creates a FK check for rows which are removed from a
   777  // table. deletedRows is used as the input to the deletion check, and deleteCols
   778  // is a list of the columns for the rows being deleted, containing values for
   779  // the referenced FK columns in the table we are mutating.
   780  func (h *fkCheckHelper) buildDeletionCheck(
   781  	deletedRows memo.RelExpr, deleteCols opt.ColList,
   782  ) memo.FKChecksItem {
   783  	// Build a semi join, with the referenced FK columns on the left and the
   784  	// origin columns on the right.
   785  	scanScope, origTabMeta := h.buildOtherTableScan()
   786  
   787  	// Note that it's impossible to orphan a row whose FK key columns contain a
   788  	// NULL, since by definition a NULL never refers to an actual row (in
   789  	// either MATCH FULL or MATCH SIMPLE).
   790  	// Build the join filters:
   791  	//   (origin_a = referenced_a) AND (origin_b = referenced_b) AND ...
   792  	f := h.mb.b.factory
   793  	semiJoinFilters := make(memo.FiltersExpr, len(deleteCols))
   794  	for j := range deleteCols {
   795  		semiJoinFilters[j] = f.ConstructFiltersItem(
   796  			f.ConstructEq(
   797  				f.ConstructVariable(deleteCols[j]),
   798  				f.ConstructVariable(scanScope.cols[j].id),
   799  			),
   800  		)
   801  	}
   802  	semiJoin := f.ConstructSemiJoin(
   803  		deletedRows, scanScope.expr, semiJoinFilters, &memo.JoinPrivate{},
   804  	)
   805  
   806  	return f.ConstructFKChecksItem(semiJoin, &memo.FKChecksItemPrivate{
   807  		OriginTable:     origTabMeta.MetaID,
   808  		ReferencedTable: h.mb.tabID,
   809  		FKOutbound:      false,
   810  		FKOrdinal:       h.fkOrdinal,
   811  		KeyCols:         deleteCols,
   812  		OpName:          h.mb.opName,
   813  	})
   814  }
   815  
   816  // setFKFallback enables fallback to the legacy foreign key checks and
   817  // cascade path.
   818  func (mb *mutationBuilder) setFKFallback() {
   819  	// Clear out any checks or cascades that may have been built already.
   820  	mb.checks = nil
   821  	mb.cascades = nil
   822  	mb.fkFallback = true
   823  	telemetry.Inc(sqltelemetry.ForeignKeyLegacyUseCounter)
   824  }