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

     1  // Copyright 2019 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 row
    12  
    13  import (
    14  	"context"
    15  
    16  	"github.com/cockroachdb/cockroach/pkg/sql/catalog"
    17  	"github.com/cockroachdb/cockroach/pkg/sql/privilege"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    19  )
    20  
    21  // The facilities in this file serve as interface between the FK
    22  // planning code and the SQL schema. They provide a cache of the
    23  // mapping between table ID and table metadata.
    24  //
    25  // Only the table metadata used for FK work are considered here.
    26  // Because CASCADE actions can affect arbitrary many tables, possibly
    27  // in cycles, the analysis algorithm to load the metadata uses a queue
    28  // (tableLookupQueue) instead of a naive recursion.
    29  //
    30  
    31  //
    32  // ------- interface between prepare and execution of FK work --------
    33  //
    34  
    35  // FkTableMetadata maps table IDs to looked up descriptors or, for tables that
    36  // exist but are not yet public/leasable, entries with just the IsAdding flag.
    37  //
    38  // This is populated by the lookup queue (below) and used as input to
    39  // the FK existence checkers and cascading actions.
    40  //
    41  // TODO(knz): the redundancy between this struct and the code in other
    42  // packages (sql, sqlbase) is troubling! Some of this should be
    43  // factored.
    44  type FkTableMetadata map[TableID]catalog.TableEntry
    45  
    46  //
    47  // ------- table metadata lookup logic, used at start of query execution -------
    48  //
    49  
    50  // TableID is an alias for sqlbase.TableID (table IDs).
    51  type TableID = sqlbase.ID
    52  
    53  // tableLookupQueue is the facility responsible for loading all
    54  // the table metadata used by FK work into a FkTableMetadata.
    55  //
    56  // The main lookup loop in MakeFkMetadata repeats as follows: run
    57  // dequeue() once, inspects the table, queue()s zero or more FK
    58  // constraints for further lookups. The lookup stops
    59  // when the queue becomes empty.
    60  type tableLookupQueue struct {
    61  	// queue contains the remaining lookups to perform.
    62  	queue []tableLookupQueueElement
    63  
    64  	// alreadyChecked notes which tables / constraints have already been
    65  	// looked up, to avoid performing the same lookup work twice.
    66  	alreadyChecked map[TableID]map[FKCheckType]struct{}
    67  
    68  	// result contains the result of the overall lookup work.
    69  	result FkTableMetadata
    70  
    71  	// tblLookupFn is used to look up individual tables by ID. This
    72  	// is typically provided by the caller, e.g. from the functions
    73  	// in the `sql` package.
    74  	tblLookupFn TableLookupFunction
    75  
    76  	// privCheckFn is used to verify a table's privileges. This is
    77  	// typically provided by the caller, e.g. from the functions in the
    78  	// `sql` package.
    79  	privCheckFn CheckPrivilegeFunction
    80  
    81  	// analyzeExprFn is used to perform semantic analysis on scalar
    82  	// expressions. This is not used for FK work directly but needed
    83  	// during lookup to initialize the CHECK constraint helper in each
    84  	// TableEntry object.
    85  	analyzeExprFn sqlbase.AnalyzeExprFunction
    86  }
    87  
    88  // tableLookupQueueElement describes one unit of work in the lookup
    89  // queue.
    90  type tableLookupQueueElement struct {
    91  	// tableEntry is the metadata of the table to check for FK
    92  	// constraints.
    93  	tableEntry catalog.TableEntry
    94  
    95  	// usage is the type of mutation for which to look up additional
    96  	// metadata. At the top level this is the type of SQL statement
    97  	// performing a mutation. Then when there are CASCADE clauses
    98  	// this is used to indicate the type of CASCADE action.
    99  	usage FKCheckType
   100  }
   101  
   102  // FKCheckType indicates the type of mutation that triggers FK work
   103  // (delete, insert, or both).
   104  type FKCheckType int
   105  
   106  const (
   107  	// CheckDeletes checks if rows reference a changed value.
   108  	CheckDeletes FKCheckType = iota
   109  	// CheckInserts checks if a new value references an existing row.
   110  	CheckInserts
   111  	// CheckUpdates checks all references (CheckDeletes+CheckInserts).
   112  	CheckUpdates
   113  )
   114  
   115  // TableLookupFunction is the function type used by MakeFkMetadata
   116  // that will perform the actual lookup of table metadata.
   117  type TableLookupFunction func(context.Context, TableID) (catalog.TableEntry, error)
   118  
   119  // NoLookup is a stub that can be used to not actually fetch metadata.
   120  // This can be used when the FK work is initialized from a pre-populated
   121  // FkTableMetadata map.
   122  func NoLookup(_ context.Context, _ TableID) (catalog.TableEntry, error) {
   123  	return catalog.TableEntry{}, nil
   124  }
   125  
   126  // CheckPrivilegeFunction is the function type used by MakeFkMetadata that will
   127  // check the privileges of the current user to access specific tables.
   128  type CheckPrivilegeFunction func(context.Context, sqlbase.DescriptorProto, privilege.Kind) error
   129  
   130  // NoCheckPrivilege is a stub that can be used to not actually verify privileges.
   131  // This can be used when the FK work is initialized from a pre-populated
   132  // FkTableMetadata map.
   133  func NoCheckPrivilege(_ context.Context, _ sqlbase.DescriptorProto, _ privilege.Kind) error {
   134  	return nil
   135  }
   136  
   137  // getTable retrieves one table's metadata during FK work preparation.
   138  // A cached TableEntry, if one exists, is reused; otherwise it is
   139  // created and initialized.
   140  func (q *tableLookupQueue) getTable(
   141  	ctx context.Context, tableID TableID,
   142  ) (catalog.TableEntry, error) {
   143  	// Do we already have an entry for this table?
   144  	if tableEntry, exists := q.result[tableID]; exists {
   145  		// Yes, simply reuse it.
   146  		return tableEntry, nil
   147  	}
   148  
   149  	// We don't have this table yet.
   150  
   151  	// Ask the caller to retrieve it for us.
   152  	tableEntry, err := q.tblLookupFn(ctx, tableID)
   153  	if err != nil {
   154  		return catalog.TableEntry{}, err
   155  	}
   156  	if !tableEntry.IsAdding && tableEntry.Desc != nil {
   157  		// If we have a real table, we need first to verify the user has permission.
   158  		if err := q.privCheckFn(ctx, tableEntry.Desc, privilege.SELECT); err != nil {
   159  			return catalog.TableEntry{}, err
   160  		}
   161  
   162  		// All is fine. Simply prepare the CHECK helper for when there are
   163  		// CASCADE actions.
   164  		//
   165  		// TODO(knz): the CHECK helper is always prepared here, even when
   166  		// there is no CASCADE work to perform. This should be moved to a
   167  		// different place.
   168  		checkHelper, err := sqlbase.NewEvalCheckHelper(ctx, q.analyzeExprFn, tableEntry.Desc)
   169  		if err != nil {
   170  			return catalog.TableEntry{}, err
   171  		}
   172  		tableEntry.CheckHelper = checkHelper
   173  	}
   174  
   175  	// Remember for next time.
   176  	q.result[tableID] = tableEntry
   177  
   178  	return tableEntry, nil
   179  }
   180  
   181  // enqueue prepares the lookup work for a given table.
   182  func (q *tableLookupQueue) enqueue(ctx context.Context, tableID TableID, usage FKCheckType) error {
   183  	// Lookup the table.
   184  	tableEntry, err := q.getTable(ctx, tableID)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	// Don't enqueue if lookup returns an empty tableEntry. This just means that
   190  	// there is no need to walk any further.
   191  	if tableEntry.Desc == nil {
   192  		return nil
   193  	}
   194  
   195  	// Only enqueue checks that haven't been performed yet.
   196  	if alreadyCheckByTableID, exists := q.alreadyChecked[tableID]; exists {
   197  		if _, existsInner := alreadyCheckByTableID[usage]; existsInner {
   198  			return nil
   199  		}
   200  	} else {
   201  		q.alreadyChecked[tableID] = make(map[FKCheckType]struct{})
   202  	}
   203  
   204  	// Remember we've done this check already for later.
   205  	q.alreadyChecked[tableID][usage] = struct{}{}
   206  
   207  	// If the table is being added, there's no need to check it.
   208  	if tableEntry.IsAdding {
   209  		return nil
   210  	}
   211  
   212  	// Verify the user has privilege to perform the operations.
   213  	switch usage {
   214  	// We only need to check the privileges for CASCADE actions here:
   215  	// the privileges related to the main mutation statement are checked
   216  	// already in that mutation's planning code.
   217  	// Also, there is no CASCADE action that can insert new rows.
   218  	case CheckDeletes:
   219  		if err := q.privCheckFn(ctx, tableEntry.Desc, privilege.DELETE); err != nil {
   220  			return err
   221  		}
   222  	case CheckUpdates:
   223  		if err := q.privCheckFn(ctx, tableEntry.Desc, privilege.UPDATE); err != nil {
   224  			return err
   225  		}
   226  	}
   227  
   228  	// Queue more lookup processing.
   229  	(*q).queue = append((*q).queue, tableLookupQueueElement{tableEntry: tableEntry, usage: usage})
   230  
   231  	return nil
   232  }
   233  
   234  // dequeue retrieves the next item in the queue (and pops it).
   235  func (q *tableLookupQueue) dequeue() (catalog.TableEntry, FKCheckType, bool) {
   236  	if len((*q).queue) == 0 {
   237  		return catalog.TableEntry{}, 0, false
   238  	}
   239  	elem := (*q).queue[0]
   240  	(*q).queue = (*q).queue[1:]
   241  	return elem.tableEntry, elem.usage, true
   242  }