docs: def compile_attribute(line, in_key, foreign_key_sql):
      """
      Convert attribute definition from DataJoint format to SQL
  
      :param line: attribution line
      :param in_key: set to True if attribute is in primary key set
      :param foreign_key_sql:
      :returns: (name, sql, is_external) -- attribute name and sql code for its declaration
      """
      try:
          match = attribute_parser.parseString(line+'#', parseAll=True)
      except pp.ParseException as err:
          raise DataJointError('Declaration error in position {pos} in line:\n  {line}\n{msg}'.format(
              line=err.args[0], pos=err.args[1], msg=err.args[2]))
      match['comment'] = match['comment'].rstrip('#')
      if 'default' not in match:
          match['default'] = ''
      match = {k: v.strip() for k, v in match.items()}
      match['nullable'] = match['default'].lower() == 'null'
      accepted_datatype = r'time|date|year|enum|(var)?char|float|real|double|decimal|numeric|' \
                          r'(tiny|small|medium|big)?int|bool|' \
                          r'(tiny|small|medium|long)?blob|external|attach'
      if re.match(accepted_datatype, match['type'], re.I) is None:
          raise DataJointError('DataJoint does not support datatype "{type}"'.format(**match))
  
      literals = ['CURRENT_TIMESTAMP']   # not to be enclosed in quotes
      if match['nullable']:
          if in_key:
              raise DataJointError('Primary key attributes cannot be nullable in line %s' % line)
          match['default'] = 'DEFAULT NULL'  # nullable attributes default to null
      else:
          if match['default']:
              quote = match['default'].upper() not in literals and match['default'][0] not in '"\''
              match['default'] = ('NOT NULL DEFAULT ' +
                                  ('"%s"' if quote else "%s") % match['default'])
          else:
              match['default'] = 'NOT NULL'
      match['comment'] = match['comment'].replace('"', '\\"')   # escape double quotes in comment
  
      is_external = match['type'].startswith('external')
      is_attachment = match['type'].startswith('attachment')
      if not is_external:
          sql = ('`{name}` {type} {default}' + (' COMMENT "{comment}"' if match['comment'] else '')).format(**match)
      else:
          # process externally stored attribute
          if in_key:
              raise DataJointError('External attributes cannot be primary in:\n%s' % line)
          store_name = match['type'].split('-')
          if store_name[0] != 'external':
              raise DataJointError('External store types must be specified as "external" or "external-<name>"')
          store_name = '-'.join(store_name[1:])
          if store_name != '' and not store_name.isidentifier():
              raise DataJointError(
                  'The external store name `{type}` is invalid. Make like a python identifier.'.format(**match))
          if len(store_name) > STORE_NAME_LENGTH:
              raise DataJointError(
                  'The external store name `{type}` is too long. Must be <={max_len} characters.'.format(
                      max_len=STORE_NAME_LENGTH, **match))
          if not match['default'] in ('DEFAULT NULL', 'NOT NULL'):
              raise DataJointError('The only acceptable default value for an external field is null in:\n%s' % line)
          if match['type'] not in config:
              raise DataJointError('The external store `{type}` is not configured.'.format(**match))
  
          # append external configuration name to the end of the comment
          sql = '`{name}` {hash_type} {default} COMMENT ":{type}:{comment}"'.format(
              hash_type=HASH_DATA_TYPE, **match)
          foreign_key_sql.append(
              "FOREIGN KEY (`{name}`) REFERENCES {{external_table}} (`hash`) "
              "ON UPDATE RESTRICT ON DELETE RESTRICT".format(**match))
  
      return match['name'], sql, is_external