docs: def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreign_key_sql, index_sql):
      """
      :param line: a line from a table definition
      :param context: namespace containing referenced objects
      :param attributes: list of attribute names already in the declaration -- to be updated by this function
      :param primary_key: None if the current foreign key is made from the dependent section. Otherwise it is the list
          of primary key attributes thus far -- to be updated by the function
      :param attr_sql: list of sql statements defining attributes -- to be updated by this function.
      :param foreign_key_sql: list of sql statements specifying foreign key constraints -- to be updated by this function.
      :param index_sql: list of INDEX declaration statements, duplicate or redundant indexes are ok.
      """
      # Parse and validate
      from .table import Table
      from .expression import Projection
  
      new_style = True   # See issue #436.  Old style to be deprecated in a future release
      try:
          result = foreign_key_parser.parseString(line)
      except pp.ParseException:
          try:
              result = foreign_key_parser_old.parseString(line)
          except pp.ParseBaseException as err:
              raise DataJointError('Parsing error in line "%s". %s.' % (line, err)) from None
          else:
              new_style = False
      try:
          ref = eval(result.ref_table, context)
      except Exception if new_style else NameError:
          raise DataJointError('Foreign key reference %s could not be resolved' % result.ref_table)
  
      options = [opt.upper() for opt in result.options]
      for opt in options:  # check for invalid options
          if opt not in {'NULLABLE', 'UNIQUE'}:
              raise DataJointError('Invalid foreign key option "{opt}"'.format(opt=opt))
      is_nullable = 'NULLABLE' in options
      is_unique = 'UNIQUE' in options
      if is_nullable and primary_key is not None:
          raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line))
  
      if not new_style:
          if not isinstance(ref, type) or not issubclass(ref, Table):
              raise DataJointError('Foreign key reference %r must be a valid query' % result.ref_table)
  
      if isinstance(ref, type) and issubclass(ref, Table):
          ref = ref()
  
      # check that dependency is of supported type
      if (not isinstance(ref, (Table, Projection)) or len(ref.restriction) or
              (isinstance(ref, Projection) and (not isinstance(ref._arg, Table) or len(ref._arg.restriction)))):
          raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' %
                               result.ref_table)
  
      if not new_style:
          # for backward compatibility with old-style dependency declarations.  See issue #436
          if not isinstance(ref, Table):
              DataJointError('Dependency "%s" is not supported. Check documentation.' % result.ref_table)
          if not all(r in ref.primary_key for r in result.ref_attrs):
              raise DataJointError('Invalid foreign key attributes in "%s"' % line)
          try:
              raise DataJointError('Duplicate attributes "{attr}" in "{line}"'.format(
                  attr=next(attr for attr in result.new_attrs if attr in attributes),
                  line=line))
          except StopIteration:
              pass   # the normal outcome
  
          # Match the primary attributes of the referenced table to local attributes
          new_attrs = list(result.new_attrs)
          ref_attrs = list(result.ref_attrs)
  
          # special case, the renamed attribute is implicit
          if new_attrs and not ref_attrs:
              if len(new_attrs) != 1:
                  raise DataJointError('Renamed foreign key must be mapped to the primary key in "%s"' % line)
              if len(ref.primary_key) == 1:
                  # if the primary key has one attribute, allow implicit renaming
                  ref_attrs = ref.primary_key
              else:
                  # if only one primary key attribute remains, then allow implicit renaming
                  ref_attrs = [attr for attr in ref.primary_key if attr not in attributes]
                  if len(ref_attrs) != 1:
                      raise DataJointError('Could not resovle which primary key attribute should be referenced in "%s"' % line)
  
          if len(new_attrs) != len(ref_attrs):
              raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
  
          if ref_attrs:
              # convert to projected dependency
              ref = ref.proj(**dict(zip(new_attrs, ref_attrs)))
  
      # declare new foreign key attributes
      base = ref._arg if isinstance(ref, Projection) else ref   # base reference table
      for attr, ref_attr in zip(ref.primary_key, base.primary_key):
          if attr not in attributes:
              attributes.append(attr)
              if primary_key is not None:
                  primary_key.append(attr)
              attr_sql.append(
                  base.heading[ref_attr].sql.replace(ref_attr, attr, 1).replace('NOT NULL ', '', int(is_nullable)))
  
      # declare the foreign key
      foreign_key_sql.append(
          'FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT'.format(
              fk='`,`'.join(ref.primary_key),
              pk='`,`'.join(base.primary_key),
              ref=base.full_table_name))
  
      # declare unique index
      if is_unique:
          index_sql.append('UNIQUE INDEX ({attrs})'.format(attrs='`,`'.join(ref.primary_key)))