# View of /Sprout/ERDB.pm

  
Branch: **MAIN**   
Changes since **1.53: +1 -0 lines**
    
    
    package ERDB;
    
        use strict;
        use Tracer;
        use DBrtns;
        use Data::Dumper;
        use XML::Simple;
        use DBQuery;
        use DBObject;
        use Stats;
        use Time::HiRes qw(gettimeofday);
        use Digest::MD5 qw(md5_base64);
        use FIG;
    
    =head1 Entity-Relationship Database Package
    
    =head2 Introduction
    
    The Entity-Relationship Database Package allows the client to create an easily-configurable
    database of Entities connected by Relationships. Each entity is represented by one or more
    relations in an underlying SQL database. Each relationship is represented by a single
    relation that connects two entities.
    
    Although this package is designed for general use, all examples are derived from the
    Sprout database, which is the first database implemented using this package.
    
    Each entity has at least one relation, the I<primary relation>, that has the same name as
    the entity. The primary relation contains a field named C<id> that contains the unique
    identifier of each entity instance. An entity may have additional relations that contain
    fields which are optional or can occur more than once. For example, the B<FEATURE> entity
    has a B<feature-type> attribute that occurs exactly once for each feature. This attribute
    is implemented by a C<feature_type> column in the primary relation C<Feature>. In addition,
    however, a feature may have zero or more aliases. These are implemented using a C<FeatureAlias>
    relation that contains two fields-- the feature ID (C<id>) and the alias name (C<alias>).
    The B<FEATURE> entity also contains an optional virulence number. This is implemented
    as a separate relation C<FeatureVirulence> which contains an ID (C<id>) and a virulence number
    (C<virulence>). If the virulence of a feature I<ABC> is known to be 6, there will be one row in
    the C<FeatureVirulence> relation possessing the value I<ABC> as its ID and 6 as its virulence
    number. If the virulence of I<ABC> is not known, there will not be any rows for it in
    C<FeatureVirulence>.
    
    Entities are connected by binary relationships implemented using single relations possessing the
    same name as the relationship itself and that has an I<arity> of 1-to-1 (C<11>), 1-to-many (C<1M>),
    or many-to-many (C<MM>). Each relationship's relation contains a C<from-link> field that contains the
    ID of the source entity and a C<to-link> field that contains the ID of the target entity. The name
    of the relationship is generally a verb phrase with the source entity as the subject and the
    target entity as the object. So, for example, the B<ComesFrom> relationship connects the B<GENOME>
    and B<SOURCE> entities, and indicates that a particular source organization participated in the
    mapping of the genome. A source organization frequently participates in the mapping
    of many genomes, and many source organizations can cooperate in the mapping of a single genome, so
    this relationship has an arity of many-to-many (C<MM>). The relation that implements the B<ComesFrom>
    relationship is called C<ComesFrom> and contains two fields-- C<from-link>, which contains a genome ID,
    and C<to-link>, which contains a source ID.
    
    A relationship may itself have attributes. These attributes, known as I<intersection data attributes>,
    are implemented as additional fields in the relationship's relation. So, for example, the
    B<IsMadeUpOf> relationship connects the B<Contig> entity to the B<Sequence> entity, and is used
    to determine which sequences make up a contig. The relationship has as an attribute the
    B<start-position>, which indicates where in the contig that the sequence begins. This attribute
    is implemented as the C<start_position> field in the C<IsMadeUpOf> relation.
    
    The database itself is described by an XML file using the F<ERDatabase.xsd> schema. In addition to
    all the data required to define the entities, relationships, and attributes, the schema provides
    space for notes describing the data and what it means. These notes are used by L</ShowMetaData>
    to generate documentation for the database.
    
    Finally, every entity and relationship object has a flag indicating if it is new or old. The object
    is considered I<old> if it was loaded by the L</LoadTables> method. It is considered I<new> if it
    was inserted by the L</InsertObject> method.
    
    To facilitate testing, the ERDB module supports automatic generation of test data. This process
    is described in the L</GenerateEntity> and L</GenerateConnection> methods, though it is not yet
    fully implemented.
    
    =head2 XML Database Description
    
    =head3 Data Types
    
    The ERDB system supports the following data types. Note that there are numerous string
    types depending on the maximum length. Some database packages limit the total number of
    characters you have in an index key; to insure the database works in all environments,
    the type of string should be the shortest one possible that supports all the known values.
    
    =over 4
    
    =item char
    
    single ASCII character
    
    =item int
    
    32-bit signed integer
    
    =item date
    
    64-bit unsigned integer, representing a PERL date/time value
    
    =item text
    
    long string; Text fields cannot be used in indexes or sorting and do not support the
    normal syntax of filter clauses, but can be up to a billion character in length
    
    =item float
    
    double-precision floating-point number
    
    =item boolean
    
    single-bit numeric value; The value is stored as a 16-bit signed integer (for
    compatability with certain database packages), but the only values supported are
    0 and 1.
    
    =item id-string
    
    variable-length string, maximum 25 characters
    
    =item key-string
    
    variable-length string, maximum 40 characters
    
    =item name-string
    
    variable-length string, maximum 80 characters
    
    =item medium-string
    
    variable-length string, maximum 160 characters
    
    =item string
    
    variable-length string, maximum 255 characters
    
    =item hash-string
    
    variable-length string, maximum 22 characters
    
    =back
    
    The hash-string data type has a special meaning. The actual key passed into the loader will
    be a string, but it will be digested into a 22-character MD5 code to save space. Although the
    MD5 algorithm is not perfect, it is extremely unlikely two strings will have the same
    digest. Therefore, it is presumed the keys will be unique. When the database is actually
    in use, the hashed keys will be presented rather than the original values. For this reason,
    they should not be used for entities where the key is meaningful.
    
    =head3 Global Tags
    
    The entire database definition must be inside a B<Database> tag. The display name of
    the database is given by the text associated with the B<Title> tag. The display name
    is only used in the automated documentation. It has no other effect. The entities and
    relationships are listed inside the B<Entities> and B<Relationships> tags,
    respectively. None of these tags have attributes.
    
        <Database>
            <Title>... display title here...</Title>
            <Entities>
                ... entity definitions here ...
            </Entities>
            <Relationships>
                ... relationship definitions here...
            </Relationships>
        </Database>
    
    Entities, relationships, indexes, and fields all allow a text tag called B<Notes>.
    The text inside the B<Notes> tag contains comments that will appear when the database
    documentation is generated. Within a B<Notes> tag, you may use C<[i]> and C<[/i]> for
    italics, C<[b]> and C<[/b]> for bold, and C<[p]> for a new paragraph.
    
    =head3 Fields
    
    Both entities and relationships have fields described by B<Field> tags. A B<Field>
    tag can have B<Notes> associated with it. The complete set of B<Field> tags for an
    object mus be inside B<Fields> tags.
    
        <Entity ... >
            <Fields>
                ... Field tags ...
            </Fields>
        </Entity>
    
    The attributes for the B<Field> tag are as follows.
    
    =over 4
    
    =item name
    
    Name of the field. The field name should contain only letters, digits, and hyphens (C<->),
    and the first character should be a letter. Most underlying databases are case-insensitive
    with the respect to field names, so a best practice is to use lower-case letters only.
    
    =item type
    
    Data type of the field. The legal data types are given above.
    
    =item relation
    
    Name of the relation containing the field. This should only be specified for entity
    fields. The ERDB system does not support optional fields or multi-occurring fields
    in the primary relation of an entity. Instead, they are put into secondary relations.
    So, for example, in the C<Genome> entity, the C<group-name> field indicates a special
    grouping used to select a subset of the genomes. A given genome may not be in any
    groups or may be in multiple groups. Therefore, C<group-name> specifies a relation
    value. The relation name specified must be a valid table name. By convention, it is
    usually the entity name followed by a qualifying word (e.g. C<GenomeGroup>). In an
    entity, the fields without a relation attribute are said to belong to the
    I<primary relation>. This relation has the same name as the entity itself.
    
    =back
    
    =head3 Indexes
    
    An entity can have multiple alternate indexes associated with it. The fields must
    be from the primary relation. The alternate indexes assist in ordering results
    from a query. A relationship can have up to two indexes-- a I<to-index> and a
    I<from-index>. These order the results when crossing the relationship. For
    example, in the relationship C<HasContig> from C<Genome> to C<Contig>, the
    from-index would order the contigs of a ganome, and the to-index would order
    the genomes of a contig. A relationship's index must specify only fields in
    the relationship.
    
    The indexes for an entity must be listed inside the B<Indexes> tag. The from-index
    of a relationship is specified using the B<FromIndex> tag; the to-index is specified
    using the B<ToIndex> tag.
    
    Each index can contain a B<Notes> tag. In addition, it will have an B<IndexFields>
    tag containing the B<IndexField> tags. These specify, in order, the fields used in
    the index. The attributes of an B<IndexField> tag are as follows.
    
    =over 4
    
    =item name
    
    Name of the field.
    
    =item order
    
    Sort order of the field-- C<ascending> or C<descending>.
    
    =back
    
    The B<Index>, B<FromIndex>, and B<ToIndex> tags themselves have no attributes.
    
    =head3 Object and Field Names
    
    By convention entity and relationship names use capital casing (e.g. C<Genome> or
    C<HasRegionsIn>. Most underlying databases, however, are aggressively case-insensitive
    with respect to relation names, converting them internally to all-upper case or
    all-lower case.
    
    If syntax or parsing errors occur when you try to load or use an ERDB database, the
    most likely reason is that one of your objects has an SQL reserved word as its name.
    The list of SQL reserved words keeps increasing; however, most are unlikely to show
    up as a noun or declarative verb phrase. The exceptions are C<Group>, C<User>,
    C<Table>, C<Index>, C<Object>, C<Date>, C<Number>, C<Update>, C<Time>, C<Percent>,
    C<Memo>, C<Order>, and C<Sum>. This problem can crop up in field names as well.
    
    Every entity has a field called C<id> that acts as its primary key. Every relationship
    has fields called C<from-link> and C<to-link> that contain copies of the relevant
    entity IDs. These are essentially ERDB's reserved words, and should not be used
    for user-defined field names.
    
    =head3 Entities
    
    An entity is described by the B<Entity> tag. The entity can contain B<Notes>, an
    B<Indexes> tag containing one or more secondary indexes, and a B<Fields> tag
    containing one or more fields. The attributes of the B<Entity> tag are as follows.
    
    =over 4
    
    =item name
    
    Name of the entity. The entity name, by convention, uses capital casing (e.g. C<Genome>
    or C<GroupBlock>) and should be a noun or noun phrase.
    
    =item keyType
    
    Data type of the primary key. The primary key is always named C<id>.
    
    =back
    
    =head3 Relationships
    
    A relationship is described by the C<Relationship> tag. Within a relationship,
    there can be a C<Notes> tag, a C<Fields> tag containing the intersection data
    fields, a C<FromIndex> tag containing the from-index, and a C<ToIndex> tag containing
    the to-index.
    
    The C<Relationship> tag has the following attributes.
    
    =over 4
    
    =item name
    
    Name of the relationship. The relationship name, by convention, uses capital casing
    (e.g. C<ContainsRegionIn> or C<HasContig>), and should be a declarative verb
    phrase, designed to fit between the from-entity and the to-entity (e.g.
    Block C<ContainsRegionIn> Genome).
    
    =item from
    
    Name of the entity from which the relationship starts.
    
    =item to
    
    Name of the entity to which the relationship proceeds.
    
    =item arity
    
    Relationship type: C<1M> for one-to-many and C<MM> for many-to-many.
    
    =back
    
    =cut
    
    # GLOBALS
    
    # Table of information about our datatypes. "sqlType" is the corresponding SQL datatype string.
    # "maxLen" is the maximum permissible length of the incoming string data used to populate a field
    # of the specified type. "dataGen" is PERL string that will be evaluated if no test data generation
    # string is specified in the field definition. "avgLen" is the average byte length for estimating
    # record sizes.
    my %TypeTable = ( char =>    { sqlType => 'CHAR(1)',            maxLen => 1,            avgLen =>   1, dataGen => "StringGen('A')" },
                      int =>     { sqlType => 'INTEGER',            maxLen => 20,           avgLen =>   4, dataGen => "IntGen(0, 99999999)" },
                      string =>  { sqlType => 'VARCHAR(255)',       maxLen => 255,          avgLen => 100, dataGen => "StringGen(IntGen(10,250))" },
                      text =>    { sqlType => 'TEXT',               maxLen => 1000000000,   avgLen => 500, dataGen => "StringGen(IntGen(80,1000))" },
                      date =>    { sqlType => 'BIGINT',             maxLen => 80,           avgLen =>   8, dataGen => "DateGen(-7, 7, IntGen(0,1400))" },
                      float =>   { sqlType => 'DOUBLE PRECISION',   maxLen => 40,           avgLen =>   8, dataGen => "FloatGen(0.0, 100.0)" },
                      boolean => { sqlType => 'SMALLINT',           maxLen => 1,            avgLen =>   1, dataGen => "IntGen(0, 1)" },
                     'hash-string' =>
                                 { sqlType => 'VARCHAR(22)',        maxLen => 22,           avgLen =>  22, dataGen => "SringGen(22)" },
                     'id-string' =>
                                 { sqlType => 'VARCHAR(25)',        maxLen => 25,           avgLen =>  25, dataGen => "SringGen(22)" },
                     'key-string' =>
                                 { sqlType => 'VARCHAR(40)',        maxLen => 40,           avgLen =>  10, dataGen => "StringGen(IntGen(10,40))" },
                     'name-string' =>
                                 { sqlType => 'VARCHAR(80)',        maxLen => 80,           avgLen =>  40, dataGen => "StringGen(IntGen(10,80))" },
                     'medium-string' =>
                                 { sqlType => 'VARCHAR(160)',       maxLen => 160,          avgLen =>  40, dataGen => "StringGen(IntGen(10,160))" },
                    );
    
    # Table translating arities into natural language.
    my %ArityTable = ( '11' => 'one-to-one',
                       '1M' => 'one-to-many',
                       'MM' => 'many-to-many'
                     );
    
    # Table for interpreting string patterns.
    
    my %PictureTable = ( 'A' => "abcdefghijklmnopqrstuvwxyz",
                         '9' => "0123456789",
                         'X' => "abcdefghijklmnopqrstuvwxyz0123456789",
                         'V' => "aeiou",
                         'K' => "bcdfghjklmnoprstvwxyz"
                       );
    
    =head2 Public Methods
    
    =head3 new
    
    C<< my $database = ERDB->new($dbh, $metaFileName); >>
    
    Create a new ERDB object.
    
    =over 4
    
    =item dbh
    
    DBKernel database object for the target database.
    
    =item metaFileName
    
    Name of the XML file containing the metadata.
    
    =back
    
    =cut
    
    sub new {
        # Get the parameters.
        my ($class, $dbh, $metaFileName, $options) = @_;
        # Load the meta-data.
        my $metaData = _LoadMetaData($metaFileName);
        # Create the object.
        my $self = { _dbh => $dbh,
                     _metaData => $metaData
                   };
        # Bless and return it.
        bless $self, $class;
        return $self;
    }
    
    =head3 ShowMetaData
    
    C<< $erdb->ShowMetaData($fileName); >>
    
    This method outputs a description of the database. This description can be used to help users create
    the data to be loaded into the relations.
    
    =over 4
    
    =item filename
    
    The name of the output file.
    
    =back
    
    =cut
    
    sub ShowMetaData {
        # Get the parameters.
        my ($self, $filename) = @_;
        # Get the metadata and the title string.
        my $metadata = $self->{_metaData};
        # Get the title string.
        my $title = $metadata->{Title};
        # Get the entity and relationship lists.
        my $entityList = $metadata->{Entities};
        my $relationshipList = $metadata->{Relationships};
        # Open the output file.
        open(HTMLOUT, ">$filename") || Confess("Could not open MetaData display file $filename: $!");
        Trace("Building MetaData table of contents.") if T(4);
        # Write the HTML heading stuff.
        print HTMLOUT "<html>\n<head>\n<title>$title</title>\n";
        print HTMLOUT "</head>\n<body>\n";
        # Write the documentation.
        print HTMLOUT $self->DisplayMetaData();
        # Close the document.
        print HTMLOUT "</body>\n</html>\n";
        # Close the file.
        close HTMLOUT;
    }
    
    =head3 DisplayMetaData
    
    C<< my $html = $erdb->DisplayMetaData(); >>
    
    Return an HTML description of the database. This description can be used to help users create
    the data to be loaded into the relations and form queries. The output is raw includable HTML
    without any HEAD or BODY tags.
    
    =over 4
    
    =item filename
    
    The name of the output file.
    
    =back
    
    =cut
    
    sub DisplayMetaData {
        # Get the parameters.
        my ($self) = @_;
        # Get the metadata and the title string.
        my $metadata = $self->{_metaData};
        # Get the title string.
        my $title = $metadata->{Title};
        # Get the entity and relationship lists.
        my $entityList = $metadata->{Entities};
        my $relationshipList = $metadata->{Relationships};
        # Declare the return variable.
        my $retVal = "";
        # Open the output file.
        Trace("Building MetaData table of contents.") if T(4);
        # Here we do the table of contents. It starts as an unordered list of section names. Each
        # section contains an ordered list of entity or relationship subsections.
        $retVal .= "<ul>\n<li><a href=\"#EntitiesSection\">Entities</a>\n<ol>\n";
        # Loop through the Entities, displaying a list item for each.
        foreach my $key (sort keys %{$entityList}) {
            # Display this item.
            $retVal .= "<li><a href=\"#$key\">$key</a></li>\n";
        }
        # Close off the entity section and start the relationship section.
        $retVal .= "</ol></li>\n<li><a href=\"#RelationshipsSection\">Relationships</a>\n<ol>\n";
        # Loop through the Relationships.
        foreach my $key (sort keys %{$relationshipList}) {
            # Display this item.
            my $relationshipTitle = _ComputeRelationshipSentence($key, $relationshipList->{$key});
            $retVal .= "<li><a href=\"#$key\">$relationshipTitle</a></li>\n";
        }
        # Close off the relationship section and list the join table section.
        $retVal .= "</ol></li>\n<li><a href=\"#JoinTable\">Join Table</a></li>\n";
        # Close off the table of contents itself.
        $retVal .=  "</ul>\n";
        # Now we start with the actual data. Denote we're starting the entity section.
        $retVal .= "<a name=\"EntitiesSection\"></a><h2>Entities</h2>\n";
        # Loop through the entities.
        for my $key (sort keys %{$entityList}) {
            Trace("Building MetaData entry for $key entity.") if T(4);
            # Create the entity header. It contains a bookmark and the entity name.
            $retVal .= "<a name=\"$key\"></a><h3>$key</h3>\n";
            # Get the entity data.
            my $entityData = $entityList->{$key};
            # If there's descriptive text, display it.
            if (my $notes = $entityData->{Notes}) {
                $retVal .= "<p>" . _HTMLNote($notes->{content}) . "</p>\n";
            }
            # Now we want a list of the entity's relationships. First, we set up the relationship subsection.
            $retVal .= "<h4>Relationships for <b>$key</b></h4>\n<ul>\n";
            # Loop through the relationships.
            for my $relationship (sort keys %{$relationshipList}) {
                # Get the relationship data.
                my $relationshipStructure = $relationshipList->{$relationship};
                # Only use the relationship if if has this entity in its FROM or TO fields.
                if ($relationshipStructure->{from} eq $key || $relationshipStructure->{to} eq $key) {
                    # Get the relationship sentence and append the arity.
                    my $relationshipDescription = _ComputeRelationshipSentence($relationship, $relationshipStructure);
                    # Display the relationship data.
                    $retVal .= "<li><a href=\"#$relationship\">$relationshipDescription</a></li>\n";
                }
            }
            # Close off the relationship list.
            $retVal .= "</ul>\n";
            # Get the entity's relations.
            my $relationList = $entityData->{Relations};
            # Create a header for the relation subsection.
            $retVal .= "<h4>Relations for <b>$key</b></h4>\n";
            # Loop through the relations, displaying them.
            for my $relation (sort keys %{$relationList}) {
                my $htmlString = _ShowRelationTable($relation, $relationList->{$relation});
                $retVal .= $htmlString;
            }
        }
        # Denote we're starting the relationship section.
        $retVal .= "<a name=\"RelationshipsSection\"></a><h2>Relationships</h2>\n";
        # Loop through the relationships.
        for my $key (sort keys %{$relationshipList}) {
            Trace("Building MetaData entry for $key relationship.") if T(4);
            # Get the relationship's structure.
            my $relationshipStructure = $relationshipList->{$key};
            # Create the relationship header.
            my $headerText = _ComputeRelationshipHeading($key, $relationshipStructure);
            $retVal .= "<h3><a name=\"$key\"></a>$headerText</h3>\n";
            # Get the entity names.
            my $fromEntity = $relationshipStructure->{from};
            my $toEntity = $relationshipStructure->{to};
            # Describe the relationship arity. Note there's a bit of trickiness involving recursive
            # many-to-many relationships. In a normal many-to-many we use two sentences to describe
            # the arity (one for each direction). This is a bad idea for a recursive relationship,
            # since both sentences will say the same thing.
            my $arity = $relationshipStructure->{arity};
            if ($arity eq "11") {
                $retVal .= "<p>Each <b>$fromEntity</b> relates to at most one <b>$toEntity</b>.\n";
            } else {
                $retVal .= "<p>Each <b>$fromEntity</b> relates to multiple <b>$toEntity</b>s.\n";
                if ($arity eq "MM" && $fromEntity ne $toEntity) {
                    $retVal .= "Each <b>$toEntity</b> relates to multiple <b>$fromEntity</b>s.\n";
                }
            }
            $retVal .= "</p>\n";
            # If there are notes on this relationship, display them.
            if (my $notes = $relationshipStructure->{Notes}) {
                $retVal .= "<p>" . _HTMLNote($notes->{content}) . "</p>\n";
            }
            # Generate the relationship's relation table.
            my $htmlString = _ShowRelationTable($key, $relationshipStructure->{Relations}->{$key});
            $retVal .= $htmlString;
        }
        Trace("Building MetaData join table.") if T(4);
        # Denote we're starting the join table.
        $retVal .= "<a name=\"JoinTable\"></a><h3>Join Table</h3>\n";
        # Create a table header.
        $retVal .= _OpenTable("Join Table", "Source", "Target", "Join Condition");
        # Loop through the joins.
        my $joinTable = $metadata->{Joins};
        my @joinKeys = keys %{$joinTable};
        for my $joinKey (sort @joinKeys) {
            # Separate out the source, the target, and the join clause.
            $joinKey =~ m!^([^/]+)/(.+)$!;
            my ($sourceRelation, $targetRelation) = ($1, $2);
            Trace("Join with key $joinKey is from $sourceRelation to $targetRelation.") if T(Joins => 4);
            my $source = $self->ComputeObjectSentence($sourceRelation);
            my $target = $self->ComputeObjectSentence($targetRelation);
            my $clause = $joinTable->{$joinKey};
            # Display them in a table row.
            $retVal .= "<tr><td>$source</td><td>$target</td><td>$clause</td></tr>\n";
        }
        # Close the table.
        $retVal .= _CloseTable();
        Trace("Built MetaData HTML.") if T(3);
        # Return the HTML.
        return $retVal;
    }
    
    =head3 DumpMetaData
    
    C<< $erdb->DumpMetaData(); >>
    
    Return a dump of the metadata structure.
    
    =cut
    
    sub DumpMetaData {
        # Get the parameters.
        my ($self) = @_;
        # Dump the meta-data.
        return Data::Dumper::Dumper($self->{_metaData});
    }
    
    =head3 CreateTables
    
    C<< $erdb->CreateTables(); >>
    
    This method creates the tables for the database from the metadata structure loaded by the
    constructor. It is expected this function will only be used on rare occasions, when the
    user needs to start with an empty database. Otherwise, the L</LoadTables> method can be
    used by itself with the truncate flag turned on.
    
    =cut
    
    sub CreateTables {
        # Get the parameters.
        my ($self) = @_;
        # Get the relation names.
        my @relNames = $self->GetTableNames();
        # Loop through the relations.
        for my $relationName (@relNames) {
            # Create a table for this relation.
            $self->CreateTable($relationName);
            Trace("Relation $relationName created.") if T(2);
        }
    }
    
    =head3 CreateTable
    
    C<< $erdb->CreateTable($tableName, $indexFlag, $estimatedRows); >>
    
    Create the table for a relation and optionally create its indexes.
    
    =over 4
    
    =item relationName
    
    Name of the relation (which will also be the table name).
    
    =item indexFlag
    
    TRUE if the indexes for the relation should be created, else FALSE. If FALSE,
    L</CreateIndexes> must be called later to bring the indexes into existence.
    
    =item estimatedRows (optional)
    
    If specified, the estimated maximum number of rows for the relation. This
    information allows the creation of tables using storage engines that are
    faster but require size estimates, such as MyISAM.
    
    =back
    
    =cut
    
    sub CreateTable {
        # Get the parameters.
        my ($self, $relationName, $indexFlag, $estimatedRows) = @_;
        # Get the database handle.
        my $dbh = $self->{_dbh};
        # Get the relation data and determine whether or not the relation is primary.
        my $relationData = $self->_FindRelation($relationName);
        my $rootFlag = $self->_IsPrimary($relationName);
        # Create a list of the field data.
        my @fieldList;
        for my $fieldData (@{$relationData->{Fields}}) {
            # Assemble the field name and type.
            my $fieldName = _FixName($fieldData->{name});
            my $fieldString = "$fieldName $TypeTable{$fieldData->{type}}->{sqlType} NOT NULL ";
            # Push the result into the field list.
            push @fieldList, $fieldString;
        }
        # If this is a root table, add the "new_record" flag. It defaults to 0, so
        if ($rootFlag) {
            push @fieldList, "new_record $TypeTable{boolean}->{sqlType} NOT NULL DEFAULT 0";
        }
        # Convert the field list into a comma-delimited string.
        my $fieldThing = join(', ', @fieldList);
        # Insure the table is not already there.
        $dbh->drop_table(tbl => $relationName);
        Trace("Table $relationName dropped.") if T(2);
        # If there are estimated rows, create an estimate so we can take advantage of
        # faster DB technologies.
        my $estimation = undef;
        if ($estimatedRows) {
            $estimation = [$self->EstimateRowSize($relationName), $estimatedRows];
        }
        # Create the table.
        Trace("Creating table $relationName: $fieldThing") if T(2);
        $dbh->create_table(tbl => $relationName, flds => $fieldThing, estimates => $estimation);
        Trace("Relation $relationName created in database.") if T(2);
        # If we want to build the indexes, we do it here.
        if ($indexFlag) {
            $self->CreateIndex($relationName);
        }
    }
    
    =head3 VerifyFields
    
    C<< my $count = $erdb->VerifyFields($relName, \@fieldList); >>
    
    Run through the list of proposed field values, insuring that all the character fields are
    below the maximum length. If any fields are too long, they will be truncated in place.
    
    =over 4
    
    =item relName
    
    Name of the relation for which the specified fields are destined.
    
    =item fieldList
    
    Reference to a list, in order, of the fields to be put into the relation.
    
    =item RETURN
    
    Returns the number of fields truncated.
    
    =back
    
    =cut
    
    sub VerifyFields {
        # Get the parameters.
        my ($self, $relName, $fieldList) = @_;
        # Initialize the return value.
        my $retVal = 0;
        # Get the relation definition.
        my $relData = $self->_FindRelation($relName);
        # Get the list of field descriptors.
        my $fieldTypes = $relData->{Fields};
        my $fieldCount = scalar @{$fieldTypes};
        # Loop through the two lists.
        for (my $i = 0; $i < $fieldCount; $i++) {
            # Get the type of the current field.
            my $fieldType = $fieldTypes->[$i]->{type};
            # If it's a character field, verify the length.
            if ($fieldType =~ /string/) {
                my $maxLen = $TypeTable{$fieldType}->{maxLen};
                my $oldString = $fieldList->[$i];
                if (length($oldString) > $maxLen) {
                    # Here it's too big, so we truncate it.
                    Trace("Truncating field $i in relation $relName to $maxLen characters from \"$oldString\".") if T(1);
                    $fieldList->[$i] = substr $oldString, 0, $maxLen;
                    $retVal++;
                }
            }
        }
        # Return the truncation count.
        return $retVal;
    }
    
    =head3 DigestFields
    
    C<< $erdb->DigestFields($relName, $fieldList); >>
    
    Digest the strings in the field list that correspond to data type C<hash-string> in the
    specified relation.
    
    =over 4
    
    =item relName
    
    Name of the relation to which the fields belong.
    
    =item fieldList
    
    List of field contents to be loaded into the relation.
    
    =back
    
    =cut
    #: Return Type ;
    sub DigestFields {
        # Get the parameters.
        my ($self, $relName, $fieldList) = @_;
        # Get the relation definition.
        my $relData = $self->_FindRelation($relName);
        # Get the list of field descriptors.
        my $fieldTypes = $relData->{Fields};
        my $fieldCount = scalar @{$fieldTypes};
        # Loop through the two lists.
        for (my $i = 0; $i < $fieldCount; $i++) {
            # Get the type of the current field.
            my $fieldType = $fieldTypes->[$i]->{type};
            # If it's a hash string, digest it in place.
            if ($fieldType eq 'hash-string') {
                $fieldList->[$i] = $self->DigestKey($fieldList->[$i]);
            }
        }
    }
    
    =head3 DigestKey
    
    C<< my $digested = $erdb->DigestKey($keyValue); >>
    
    Return the digested value of a symbolic key. The digested value can then be plugged into a
    key-based search into a table with key-type hash-string.
    
    Currently the digesting process is independent of the database structure, but that may not
    always be the case, so this is an instance method instead of a static method.
    
    =over 4
    
    =item keyValue
    
    Key value to digest.
    
    =item RETURN
    
    Digested value ofthe key.
    
    =back
    
    =cut
    
    sub DigestKey {
        # Get the parameters.
        my ($self, $keyValue) = @_;
        # Compute the digest.
        my $retVal = md5_base64($keyValue);
        # Return the result.
        return $retVal;
    }
    
    =head3 CreateIndex
    
    C<< $erdb->CreateIndex($relationName); >>
    
    Create the indexes for a relation. If a table is being loaded from a large source file (as
    is the case in L</LoadTable>), it is sometimes best to create the indexes after the load.
    If that is the case, then L</CreateTable> should be called with the index flag set to
    FALSE, and this method used after the load to create the indexes for the table.
    
    =cut
    
    sub CreateIndex {
        # Get the parameters.
        my ($self, $relationName) = @_;
        # Get the relation's descriptor.
        my $relationData = $self->_FindRelation($relationName);
        # Get the database handle.
        my $dbh = $self->{_dbh};
        # Now we need to create this relation's indexes. We do this by looping through its index table.
        my $indexHash = $relationData->{Indexes};
        for my $indexName (keys %{$indexHash}) {
            my $indexData = $indexHash->{$indexName};
            # Get the index's field list.
            my @fieldList = _FixNames(@{$indexData->{IndexFields}});
            my $flds = join(', ', @fieldList);
            # Get the index's uniqueness flag.
            my $unique = (exists $indexData->{Unique} ? $indexData->{Unique} : 'false');
            # Create the index.
            my $rv = $dbh->create_index(idx => $indexName, tbl => $relationName,
                                        flds => $flds, unique => $unique);
            if ($rv) {
                Trace("Index created: $indexName for $relationName ($flds)") if T(1);
            } else {
                Confess("Error creating index $indexName for $relationName using ($flds): " . $dbh->error_message());
            }
        }
    }
    
    =head3 LoadTables
    
    C<< my $stats = $erdb->LoadTables($directoryName, $rebuild); >>
    
    This method will load the database tables from a directory. The tables must already have been created
    in the database. (This can be done by calling L</CreateTables>.) The caller passes in a directory name;
    all of the relations to be loaded must have a file in the directory with the same name as the relation
    (optionally with a suffix of C<.dtx>). Each file must be a tab-delimited table of field values. Each
    line of the file will be loaded as a row of the target relation table. The field values should be in
    the same order as the fields in the relation tables generated by L</ShowMetaData>. The old data is
    erased before the new data is loaded in.
    
    A certain amount of translation automatically takes place. Ctrl-M characters are deleted, and
    tab and new-line characters inside a field are escaped as C<\t> and C<\n>, respectively. Dates must
    be entered as a Unix timestamp, that is, as an integer number of seconds since the base epoch.
    
    =over 4
    
    =item directoryName
    
    Name of the directory containing the relation files to be loaded.
    
    =item rebuild
    
    TRUE if the tables should be dropped and rebuilt, else FALSE. This is, unfortunately, the
    only way to erase existing data in the tables, since the TRUNCATE command is not supported
    by all of the DB engines we use.
    
    =item RETURN
    
    Returns a statistical object describing the number of records read and a list of the error messages.
    
    =back
    
    =cut
    
    sub LoadTables {
        # Get the parameters.
        my ($self, $directoryName, $rebuild) = @_;
        # Start the timer.
        my $startTime = gettimeofday;
        # Clean any trailing slash from the directory name.
        $directoryName =~ s!/\\$!!;
        # Declare the return variable.
        my $retVal = Stats->new();
        # Get the relation names.
        my @relNames = $self->GetTableNames();
        for my $relationName (@relNames) {
            # Try to load this relation.
            my $result = $self->_LoadRelation($directoryName, $relationName, $rebuild);
            # Accumulate the statistics.
            $retVal->Accumulate($result);
        }
        # Add the duration of the load to the statistical object.
        $retVal->Add('duration', gettimeofday - $startTime);
        # Return the accumulated statistics.
        return $retVal;
    }
    
    
    =head3 GetTableNames
    
    C<< my @names = $erdb->GetTableNames; >>
    
    Return a list of the relations required to implement this database.
    
    =cut
    
    sub GetTableNames {
        # Get the parameters.
        my ($self) = @_;
        # Get the relation list from the metadata.
        my $relationTable = $self->{_metaData}->{RelationTable};
        # Return the relation names.
        return keys %{$relationTable};
    }
    
    =head3 GetEntityTypes
    
    C<< my @names = $erdb->GetEntityTypes; >>
    
    Return a list of the entity type names.
    
    =cut
    
    sub GetEntityTypes {
        # Get the database object.
        my ($self) = @_;
        # Get the entity list from the metadata object.
        my $entityList = $self->{_metaData}->{Entities};
        # Return the list of entity names in alphabetical order.
        return sort keys %{$entityList};
    }
    
    =head3 IsEntity
    
    C<< my $flag = $erdb->IsEntity($entityName); >>
    
    Return TRUE if the parameter is an entity name, else FALSE.
    
    =over 4
    
    =item entityName
    
    Object name to be tested.
    
    =item RETURN
    
    Returns TRUE if the specified string is an entity name, else FALSE.
    
    =back
    
    =cut
    
    sub IsEntity {
        # Get the parameters.
        my ($self, $entityName) = @_;
        # Test to see if it's an entity.
        return exists $self->{_metaData}->{Entities}->{$entityName};
    }
    
    =head3 Get
    
    C<< my $query = $erdb->Get(\@objectNames, $filterClause, \@params); >>
    
    This method returns a query object for entities of a specified type using a specified filter.
    The filter is a standard WHERE/ORDER BY clause with question marks as parameter markers and each
    field name represented in the form B<I<objectName>(I<fieldName>)>. For example, the
    following call requests all B<Genome> objects for the genus specified in the variable
    $genus.
    
    C<< $query = $erdb->Get(['Genome'], "Genome(genus) = ?", [$genus]); >>
    
    The WHERE clause contains a single question mark, so there is a single additional
    parameter representing the parameter value. It would also be possible to code
    
    C<< $query = $erdb->Get(['Genome'], "Genome(genus) = \'$genus\'"); >>
    
    however, this version of the call would generate a syntax error if there were any quote
    characters inside the variable C<$genus>.
    
    The use of the strange parenthesized notation for field names enables us to distinguish
    hyphens contained within field names from minus signs that participate in the computation
    of the WHERE clause. All of the methods that manipulate fields will use this same notation.
    
    It is possible to specify multiple entity and relationship names in order to retrieve more than
    one object's data at the same time, which allows highly complex joined queries. For example,
    
    C<< $query = $erdb->Get(['Genome', 'ComesFrom', 'Source'], "Genome(genus) = ?", [$genus]); >>
    
    If multiple names are specified, then the query processor will automatically determine a
    join path between the entities and relationships. The algorithm used is very simplistic.
    In particular, if a relationship is recursive, the path is determined by the order in which
    the entity and the relationship appear. For example, consider a recursive relationship
    B<IsParentOf> which relates B<People> objects to other B<People> objects. If the join path is
    coded as C<['People', 'IsParentOf']>, then the people returned will be parents. If, however,
    the join path is C<['IsParentOf', 'People']>, then the people returned will be children.
    
    If an entity or relationship is mentioned twice, the name for the second occurrence will
    be suffixed with C<2>, the third occurrence will be suffixed with C<3>, and so forth. So,
    for example, if we have C<['Feature', 'HasContig', 'Contig', 'HasContig']>, then the
    B<to-link> field of the first B<HasContig> is specified as C<HasContig(to-link)>, while
    the B<to-link> field of the second B<HasContig> is specified as C<HasContig2(to-link)>.
    
    =over 4
    
    =item objectNames
    
    List containing the names of the entity and relationship objects to be retrieved.
    
    =item filterClause
    
    WHERE clause (without the WHERE) to be used to filter and sort the query. The WHERE clause can
    be parameterized with parameter markers (C<?>). Each field used in the WHERE clause must be
    specified in the standard form B<I<objectName>(I<fieldName>)>. Any parameters specified
    in the filter clause should be added to the parameter list as additional parameters. The
    fields in a filter clause can come from primary entity relations, relationship relations,
    or secondary entity relations; however, all of the entities and relationships involved must
    be included in the list of object names.
    
    The filter clause can also specify a sort order. To do this, simply follow the filter string
    with an ORDER BY clause. For example, the following filter string gets all genomes for a
    particular genus and sorts them by species name.
    
    C<< "Genome(genus) = ? ORDER BY Genome(species)" >>
    
    Note that the case is important. Only an uppercase "ORDER BY" with a single space will
    be processed. The idea is to make it less likely to find the verb by accident.
    
    The rules for field references in a sort order are the same as those for field references in the
    filter clause in general; however, odd things may happen if a sort field is from a secondary
    relation.
    
    Finally, you can limit the number of rows returned by adding a LIMIT clause. The LIMIT must
    be the last thing in the filter clause, and it contains only the word "LIMIT" followed by
    a positive number. So, for example
    
    C<< "Genome(genus) = ? ORDER BY Genome(species) LIMIT 10" >>
    
    will only return the first ten genomes for the specified genus. The ORDER BY clause is not
    required. For example, to just get the first 10 genomes in the B<Genome> table, you could
    use
    
    C<< "LIMIT 10" >>
    
    =item params
    
    Reference to a list of parameter values to be substituted into the filter clause.
    
    =item RETURN
    
    Returns a B<DBQuery> that can be used to iterate through all of the results.
    
    =back
    
    =cut
    
    sub Get {
        # Get the parameters.
        my ($self, $objectNames, $filterClause, $params) = @_;
        # Process the SQL stuff.
        my ($suffix, $mappedNameListRef, $mappedNameHashRef) =
            $self->_SetupSQL($objectNames, $filterClause);
        # Create the query.
        my $command = "SELECT DISTINCT " . join(".*, ", @{$mappedNameListRef}) .
            ".* $suffix";
        my $sth = $self->_GetStatementHandle($command, $params);
        # Now we create the relation map, which enables DBQuery to determine the order, name
        # and mapped name for each object in the query.
        my @relationMap = ();
        for my $mappedName (@{$mappedNameListRef}) {
            push @relationMap, [$mappedName, $mappedNameHashRef->{$mappedName}];
        }
        # Return the statement object.
        my $retVal = DBQuery::_new($self, $sth, \@relationMap);
        return $retVal;
    }
    
    =head3 GetFlat
    
    C<< my @list = $erdb->GetFlat(\@objectNames, $filterClause, \@parameterList, $field); >>
    
    This is a variation of L</GetAll> that asks for only a single field per record and
    returns a single flattened list.
    
    =over 4
    
    =item objectNames
    
    List containing the names of the entity and relationship objects to be retrieved.
    
    =item filterClause
    
    WHERE/ORDER BY clause (without the WHERE) to be used to filter and sort the query. The WHERE clause can
    be parameterized with parameter markers (C<?>). Each field used must be specified in the standard form
    B<I<objectName>(I<fieldName>)>. Any parameters specified in the filter clause should be added to the
    parameter list as additional parameters. The fields in a filter clause can come from primary
    entity relations, relationship relations, or secondary entity relations; however, all of the
    entities and relationships involved must be included in the list of object names.
    
    =item parameterList
    
    List of the parameters to be substituted in for the parameters marks in the filter clause.
    
    =item field
    
    Name of the field to be used to get the elements of the list returned.
    
    =item RETURN
    
    Returns a list of values.
    
    =back
    
    =cut
    #: Return Type @;
    sub GetFlat {
        # Get the parameters.
        my ($self, $objectNames, $filterClause, $parameterList, $field) = @_;
        # Construct the query.
        my $query = $self->Get($objectNames, $filterClause, $parameterList);
        # Create the result list.
        my @retVal = ();
        # Loop through the records, adding the field values found to the result list.
        while (my $row = $query->Fetch()) {
            push @retVal, $row->Value($field);
        }
        # Return the list created.
        return @retVal;
    }
    
    =head3 Delete
    
    C<< my $stats = $erdb->Delete($entityName, $objectID); >>
    
    Delete an entity instance from the database. The instance is deleted along with all entity and
    relationship instances dependent on it. The idea of dependence here is recursive. An object is
    always dependent on itself. An object is dependent if it is a 1-to-many or many-to-many
    relationship connected to a dependent entity or the "to" entity connected to a 1-to-many
    dependent relationship.
    
    =over 4
    
    =item entityName
    
    Name of the entity type for the instance being deleted.
    
    =item objectID
    
    ID of the entity instance to be deleted. If the ID contains a wild card character (C<%>),
    then it is presumed to by a LIKE pattern.
    
    =item testFlag
    
    If TRUE, the delete statements will be traced without being executed.
    
    =item RETURN
    
    Returns a statistics object indicating how many records of each particular table were
    deleted.
    
    =back
    
    =cut
    #: Return Type $%;
    sub Delete {
        # Get the parameters.
        my ($self, $entityName, $objectID, $testFlag) = @_;
        # Declare the return variable.
        my $retVal = Stats->new();
        # Get the DBKernel object.
        my $db = $self->{_dbh};
        # We're going to generate all the paths branching out from the starting entity. One of
        # the things we have to be careful about is preventing loops. We'll use a hash to
        # determine if we've hit a loop.
        my %alreadyFound = ();
        # These next lists will serve as our result stack. We start by pushing object lists onto
        # the stack, and then popping them off to do the deletes. This means the deletes will
        # start with the longer paths before getting to the shorter ones. That, in turn, makes
        # sure we don't delete records that might be needed to forge relationships back to the
        # original item. We have two lists-- one for TO-relationships, and one for
        # FROM-relationships and entities.
        my @fromPathList = ();
        my @toPathList = ();
        # This final hash is used to remember what work still needs to be done. We push paths
        # onto the list, then pop them off to extend the paths. We prime it with the starting
        # point. Note that we will work hard to insure that the last item on a path in the
        # TODO list is always an entity.
        my @todoList = ([$entityName]);
        while (@todoList) {
            # Get the current path.
            my $current = pop @todoList;
            # Copy it into a list.
            my @stackedPath = @{$current};
            # Pull off the last item on the path. It will always be an entity.
            my $entityName = pop @stackedPath;
            # Add it to the alreadyFound list.
            $alreadyFound{$entityName} = 1;
            # Get the entity data.
            my $entityData = $self->_GetStructure($entityName);
            # The first task is to loop through the entity's relation. A DELETE command will
            # be needed for each of them.
            my $relations = $entityData->{Relations};
            for my $relation (keys %{$relations}) {
                my @augmentedList = (@stackedPath, $relation);
                push @fromPathList, \@augmentedList;
            }
            # Now we need to look for relationships connected to this entity.
            my $relationshipList = $self->{_metaData}->{Relationships};
            for my $relationshipName (keys %{$relationshipList}) {
                my $relationship = $relationshipList->{$relationshipName};
                # Check the FROM field. We're only interested if it's us.
                if ($relationship->{from} eq $entityName) {
                    # Add the path to this relationship.
                    my @augmentedList = (@stackedPath, $entityName, $relationshipName);
                    push @fromPathList, \@augmentedList;
                    # Check the arity. If it's MM we're done. If it's 1M
                    # and the target hasn't been seen yet, we want to
                    # stack the entity for future processing.
                    if ($relationship->{arity} eq '1M') {
                        my $toEntity = $relationship->{to};
                        if (! exists $alreadyFound{$toEntity}) {
                            # Here we have a new entity that's dependent on
                            # the current entity, so we need to stack it.
                            my @stackList = (@augmentedList, $toEntity);
                            push @fromPathList, \@stackList;
                        } else {
                            Trace("$toEntity ignored because it occurred previously.") if T(4);
                        }
                    }
                }
                # Now check the TO field. In this case only the relationship needs
                # deletion.
                if ($relationship->{to} eq $entityName) {
                    my @augmentedList = (@stackedPath, $entityName, $relationshipName);
                    push @toPathList, \@augmentedList;
                }
            }
        }
        # Create the first qualifier for the WHERE clause. This selects the
        # keys of the primary entity records to be deleted. When we're deleting
        # from a dependent table, we construct a join page from the first qualifier
        # to the table containing the dependent records to delete.
        my $qualifier = ($objectID =~ /%/ ? "LIKE ?" : "= ?");
        # We need to make two passes. The first is through the to-list, and
        # the second through the from-list. The from-list is second because
        # the to-list may need to pass through some of the entities the
        # from-list would delete.
        my %stackList = ( from_link => \@fromPathList, to_link => \@toPathList );
        # Now it's time to do the deletes. We do it in two passes.
        for my $keyName ('to_link', 'from_link') {
            # Get the list for this key.
            my @pathList = @{$stackList{$keyName}};
            Trace(scalar(@pathList) . " entries in path list for $keyName.") if T(3);
            # Loop through this list.
            while (my $path = pop @pathList) {
                # Get the table whose rows are to be deleted.
                my @pathTables = @{$path};
                # Start the DELETE statement. We need to call DBKernel because the
                # syntax of a DELETE-USING varies among DBMSs.
                my $target = $pathTables[$#pathTables];
                my $stmt = $db->SetUsing(@pathTables);
                # Now start the WHERE. The first thing is the ID field from the starting table. That
                # starting table will either be the entity relation or one of the entity's
                # sub-relations.
                $stmt .= " WHERE $pathTables[0].id $qualifier";
                # Now we run through the remaining entities in the path, connecting them up.
                for (my $i = 1; $i <= $#pathTables; $i += 2) {
                    # Connect the current relationship to the preceding entity.
                    my ($entity, $rel) = @pathTables[$i-1,$i];
                    # The style of connection depends on the direction of the relationship.
                    $stmt .= " AND $entity.id = $rel.$keyName";
                    if ($i + 1 <= $#pathTables) {
                        # Here there's a next entity, so connect that to the relationship's
                        # to-link.
                        my $entity2 = $pathTables[$i+1];
                        $stmt .= " AND $rel.to_link = $entity2.id";
                    }
                }
                # Now we have our desired DELETE statement.
                if ($testFlag) {
                    # Here the user wants to trace without executing.
                    Trace($stmt) if T(0);
                } else {
                    # Here we can delete. Note that the SQL method dies with a confessing
                    # if an error occurs, so we just go ahead and do it.
                    Trace("Executing delete from $target using '$objectID'.") if T(3);
                    my $rv = $db->SQL($stmt, 0, $objectID);
                    # Accumulate the statistics for this delete. The only rows deleted
                    # are from the target table, so we use its name to record the
                    # statistic.
                    $retVal->Add($target, $rv);
                }
            }
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 GetList
    
    C<< my @dbObjects = $erdb->GetList(\@objectNames, $filterClause, \@params); >>
    
    Return a list of object descriptors for the specified objects as determined by the
    specified filter clause.
    
    This method is essentially the same as L</Get> except it returns a list of objects rather
    than a query object that can be used to get the results one record at a time.
    
    =over 4
    
    =item objectNames
    
    List containing the names of the entity and relationship objects to be retrieved.
    
    =item filterClause
    
    WHERE clause (without the WHERE) to be used to filter and sort the query. The WHERE clause can
    be parameterized with parameter markers (C<?>). Each field used in the WHERE clause must be
    specified in the standard form B<I<objectName>(I<fieldName>)>. Any parameters specified
    in the filter clause should be added to the parameter list as additional parameters. The
    fields in a filter clause can come from primary entity relations, relationship relations,
    or secondary entity relations; however, all of the entities and relationships involved must
    be included in the list of object names.
    
    The filter clause can also specify a sort order. To do this, simply follow the filter string
    with an ORDER BY clause. For example, the following filter string gets all genomes for a
    particular genus and sorts them by species name.
    
    C<< "Genome(genus) = ? ORDER BY Genome(species)" >>
    
    The rules for field references in a sort order are the same as those for field references in the
    filter clause in general; however, odd things may happen if a sort field is from a secondary
    relation.
    
    =item params
    
    Reference to a list of parameter values to be substituted into the filter clause.
    
    =item RETURN
    
    Returns a list of B<DBObject>s that satisfy the query conditions.
    
    =back
    
    =cut
    #: Return Type @%
    sub GetList {
        # Get the parameters.
        my ($self, $objectNames, $filterClause, $params) = @_;
        # Declare the return variable.
        my @retVal = ();
        # Perform the query.
        my $query = $self->Get($objectNames, $filterClause, $params);
        # Loop through the results.
        while (my $object = $query->Fetch) {
            push @retVal, $object;
        }
        # Return the result.
        return @retVal;
    }
    
    =head3 GetCount
    
    C<< my $count = $erdb->GetCount(\@objectNames, $filter, \@params); >>
    
    Return the number of rows found by a specified query. This method would
    normally be used to count the records in a single table. For example, in a
    genetics database
    
        my $count = $erdb->GetCount(['Genome'], 'Genome(genus-species) LIKE ?', ['homo %']);
    
    would return the number of genomes for the genus I<homo>. It is conceivable, however,
    to use it to return records based on a join. For example,
    
        my $count = $erdb->GetCount(['HasFeature', 'Genome'], 'Genome(genus-species) LIKE ?',
                                    ['homo %']);
    
    would return the number of features for genomes in the genus I<homo>. Note that
    only the rows from the first table are counted. If the above command were
    
        my $count = $erdb->GetCount(['Genome', 'Feature'], 'Genome(genus-species) LIKE ?',
                                    ['homo %']);
    
    it would return the number of genomes, not the number of genome/feature pairs.
    
    =over 4
    
    =item objectNames
    
    Reference to a list of the objects (entities and relationships) included in the
    query.
    
    =item filter
    
    A filter clause for restricting the query. The rules are the same as for the L</Get>
    method.
    
    =item params
    
    Reference to a list of the parameter values to be substituted for the parameter marks
    in the filter.
    
    =item RETURN
    
    Returns a count of the number of records in the first table that would satisfy
    the query.
    
    =back
    
    =cut
    
    sub GetCount {
        # Get the parameters.
        my ($self, $objectNames, $filter, $params) = @_;
        # Declare the return variable.
        my $retVal;
        # Find out if we're counting an entity or a relationship.
        my $countedField;
        if ($self->IsEntity($objectNames->[0])) {
            $countedField = "id";
        } else {
            # For a relationship we count the to-link because it's usually more
            # numerous. Note we're automatically converting to the SQL form
            # of the field name (to_link vs. to-link).
            $countedField = "to_link";
        }
        # Create the SQL command suffix to get the desired records.
        my ($suffix, $mappedNameListRef, $mappedNameHashRef) = $self->_SetupSQL($objectNames,
                                                                                $filter);
        # Prefix it with text telling it we want a record count.
        my $firstObject = $mappedNameListRef->[0];
        my $command = "SELECT COUNT($firstObject.$countedField) $suffix";
        # Prepare and execute the command.
        my $sth = $self->_GetStatementHandle($command, $params);
        # Get the count value.
        ($retVal) = $sth->fetchrow_array();
        # Check for a problem.
        if (! defined($retVal)) {
            if ($sth->err) {
                # Here we had an SQL error.
                Confess("Error retrieving row count: " . $sth->errstr());
            } else {
                # Here we have no result.
                Confess("No result attempting to retrieve row count.");
            }
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 ComputeObjectSentence
    
    C<< my $sentence = $erdb->ComputeObjectSentence($objectName); >>
    
    Check an object name, and if it is a relationship convert it to a relationship sentence.
    
    =over 4
    
    =item objectName
    
    Name of the entity or relationship.
    
    =item RETURN
    
    Returns a string containing the entity name or a relationship sentence.
    
    =back
    
    =cut
    
    sub ComputeObjectSentence {
        # Get the parameters.
        my ($self, $objectName) = @_;
        # Set the default return value.
        my $retVal = $objectName;
        # Look for the object as a relationship.
        my $relTable = $self->{_metaData}->{Relationships};
        if (exists $relTable->{$objectName}) {
            # Get the relationship sentence.
            $retVal = _ComputeRelationshipSentence($objectName, $relTable->{$objectName});
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 DumpRelations
    
    C<< $erdb->DumpRelations($outputDirectory); >>
    
    Write the contents of all the relations to tab-delimited files in the specified directory.
    Each file will have the same name as the relation dumped, with an extension of DTX.
    
    =over 4
    
    =item outputDirectory
    
    Name of the directory into which the relation files should be dumped.
    
    =back
    
    =cut
    
    sub DumpRelations {
        # Get the parameters.
        my ($self, $outputDirectory) = @_;
        # Now we need to run through all the relations. First, we loop through the entities.
        my $metaData = $self->{_metaData};
        my $entities = $metaData->{Entities};
        for my $entityName (keys %{$entities}) {
            my $entityStructure = $entities->{$entityName};
            # Get the entity's relations.
            my $relationList = $entityStructure->{Relations};
            # Loop through the relations, dumping them.
            for my $relationName (keys %{$relationList}) {
                my $relation = $relationList->{$relationName};
                $self->_DumpRelation($outputDirectory, $relationName, $relation);
            }
        }
        # Next, we loop through the relationships.
        my $relationships = $metaData->{Relationships};
        for my $relationshipName (keys %{$relationships}) {
            my $relationshipStructure = $relationships->{$relationshipName};
            # Dump this relationship's relation.
            $self->_DumpRelation($outputDirectory, $relationshipName, $relationshipStructure->{Relations}->{$relationshipName});
        }
    }
    
    =head3 InsertValue
    
    C<< $erdb->InsertValue($entityID, $fieldName, $value); >>
    
    This method will insert a new value into the database. The value must be one
    associated with a secondary relation, since primary values cannot be inserted:
    they occur exactly once. Secondary values, on the other hand, can be missing
    or multiply-occurring.
    
    =over 4
    
    =item entityID
    
    ID of the object that is to receive the new value.
    
    =item fieldName
    
    Field name for the new value-- this includes the entity name, since
    field names are of the format I<objectName>C<(>I<fieldName>C<)>.
    
    =item value
    
    New value to be put in the field.
    
    =back
    
    =cut
    
    sub InsertValue {
        # Get the parameters.
        my ($self, $entityID, $fieldName, $value) = @_;
        # Parse the entity name and the real field name.
        if ($fieldName =~ /^([^(]+)\(([^)]+)\)/) {
            my $entityName = $1;
            my $fieldTitle = $2;
            # Get its descriptor.
            if (!$self->IsEntity($entityName)) {
                Confess("$entityName is not a valid entity.");
            } else {
                my $entityData = $self->{_metaData}->{Entities}->{$entityName};
                # Find the relation containing this field.
                my $fieldHash = $entityData->{Fields};
                if (! exists $fieldHash->{$fieldTitle}) {
                    Confess("$fieldTitle not found in $entityName.");
                } else {
                    my $relation = $fieldHash->{$fieldTitle}->{relation};
                    if ($relation eq $entityName) {
                        Confess("Cannot do InsertValue on primary field $fieldTitle of $entityName.");
                    } else {
                        # Now we can create an INSERT statement.
                        my $dbh = $self->{_dbh};
                        my $fixedName = _FixName($fieldTitle);
                        my $statement = "INSERT INTO $relation (id, $fixedName) VALUES(?, ?)";
                        # Execute the command.
                        $dbh->SQL($statement, 0, $entityID, $value);
                    }
                }
            }
        } else {
            Confess("$fieldName is not a valid field name.");
        }
    }
    
    =head3 InsertObject
    
    C<< my $ok = $erdb->InsertObject($objectType, \%fieldHash); >>
    
    Insert an object into the database. The object is defined by a type name and then a hash
    of field names to values. Field values in the primary relation are represented by scalars.
    (Note that for relationships, the primary relation is the B<only> relation.)
    Field values for the other relations comprising the entity are always list references. For
    example, the following line inserts an inactive PEG feature named C<fig|188.1.peg.1> with aliases
    C<ZP_00210270.1> and C<gi|46206278>.
    
    C<< $erdb->InsertObject('Feature', { id => 'fig|188.1.peg.1', active => 0, feature-type => 'peg', alias => ['ZP_00210270.1', 'gi|46206278']}); >>
    
    The next statement inserts a C<HasProperty> relationship between feature C<fig|158879.1.peg.1> and
    property C<4> with an evidence URL of C<http://seedu.uchicago.edu/query.cgi?article_id=142>.
    
    C<< $erdb->InsertObject('HasProperty', { 'from-link' => 'fig|158879.1.peg.1', 'to-link' => 4, evidence = 'http://seedu.uchicago.edu/query.cgi?article_id=142'}); >>
    
    =over 4
    
    =item newObjectType
    
    Type name of the object to insert.
    
    =item fieldHash
    
    Hash of field names to values.
    
    =item RETURN
    
    Returns 1 if successful, 0 if an error occurred.
    
    =back
    
    =cut
    
    sub InsertObject {
        # Get the parameters.
        my ($self, $newObjectType, $fieldHash) = @_;
        # Denote that so far we appear successful.
        my $retVal = 1;
        # Get the database handle.
        my $dbh = $self->{_dbh};
        # Get the relation list.
        my $relationTable = $self->_GetRelationTable($newObjectType);
        # Loop through the relations. We'll build insert statements for each one. If a relation is
        # secondary, we may end up generating multiple insert statements. If an error occurs, we
        # stop the loop.
        my @relationList = keys %{$relationTable};
        for (my $i = 0; $retVal && $i <= $#relationList; $i++) {
            my $relationName = $relationList[$i];
            my $relationDefinition = $relationTable->{$relationName};
            # Get the relation's fields. For each field we will collect a value in the corresponding
            # position of the @valueList array. If one of the fields is missing, we will add it to the
            # @missing list.
            my @fieldList = @{$relationDefinition->{Fields}};
            my @fieldNameList = ();
            my @valueList = ();
            my @missing = ();
            my $recordCount = 1;
            for my $fieldDescriptor (@fieldList) {
                # Get the field name and save it. Note we need to fix it up so the hyphens
                # are converted to underscores.
                my $fieldName = $fieldDescriptor->{name};
                push @fieldNameList, _FixName($fieldName);
                # Look for the named field in the incoming structure. Note that we are looking
                # for the real field name, not the fixed-up one!
                if (exists $fieldHash->{$fieldName}) {
                    # Here we found the field. Stash it in the value list.
                    my $value = $fieldHash->{$fieldName};
                    push @valueList, $value;
                    # If the value is a list, we may need to increment the record count.
                    if (ref $value eq "ARRAY") {
                        my $thisCount = @{$value};
                        if ($recordCount == 1) {
                            # Here we have our first list, so we save its count.
                            $recordCount = $thisCount;
                        } elsif ($recordCount != $thisCount) {
                            # Here we have a second list, so its length has to match the
                            # previous lists.
                            Trace("Field $value in new $newObjectType object has an invalid list length $thisCount. Expected $recordCount.") if T(0);
                            $retVal = 0;
                        }
                    }
                } else {
                    # Here the field is not present. Flag it as missing.
                    push @missing, $fieldName;
                }
            }
            # If we are the primary relation, add the new-record flag.
            if ($relationName eq $newObjectType) {
                push @valueList, 1;
                push @fieldNameList, "new_record";
            }
            # Only proceed if there are no missing fields.
            if (@missing > 0) {
                Trace("Relation $relationName for $newObjectType skipped due to missing fields: " .
                    join(' ', @missing)) if T(1);
            } else {
                # Build the INSERT statement.
                my $statement = "INSERT INTO $relationName (" . join (', ', @fieldNameList) .
                    ") VALUES (";
                # Create a marker list of the proper size and put it in the statement.
                my @markers = ();
                while (@markers < @fieldNameList) { push @markers, '?'; }
                $statement .= join(', ', @markers) . ")";
                # We have the insert statement, so prepare it.
                my $sth = $dbh->prepare_command($statement);
                Trace("Insert statement prepared: $statement") if T(3);
                # Now we loop through the values. If a value is scalar, we use it unmodified. If it's
                # a list, we use the current element. The values are stored in the @parameterList array.
                my $done = 0;
                for (my $i = 0; $i < $recordCount; $i++) {
                    # Clear the parameter list array.
                    my @parameterList = ();
                    # Loop through the values.
                    for my $value (@valueList) {
                        # Check to see if this is a scalar value.
                        if (ref $value eq "ARRAY") {
                            # Here we have a list value. Pull the current entry.
                            push @parameterList, $value->[$i];
                        } else {
                            # Here we have a scalar value. Use it unmodified.
                            push @parameterList, $value;
                        }
                    }
                    # Execute the INSERT statement with the specified parameter list.
                    $retVal = $sth->execute(@parameterList);
                    if (!$retVal) {
                        my $errorString = $sth->errstr();
                        Trace("Insert error: $errorString.") if T(0);
                    }
                }
            }
        }
        # Return the success indicator.
        return $retVal;
    }
    
    =head3 LoadTable
    
    C<< my %results = $erdb->LoadTable($fileName, $relationName, $truncateFlag); >>
    
    Load data from a tab-delimited file into a specified table, optionally re-creating the table
    first.
    
    =over 4
    
    =item fileName
    
    Name of the file from which the table data should be loaded.
    
    =item relationName
    
    Name of the relation to be loaded. This is the same as the table name.
    
    =item truncateFlag
    
    TRUE if the table should be dropped and re-created, else FALSE
    
    =item RETURN
    
    Returns a statistical object containing a list of the error messages.
    
    =back
    
    =cut
    sub LoadTable {
        # Get the parameters.
        my ($self, $fileName, $relationName, $truncateFlag) = @_;
        # Create the statistical return object.
        my $retVal = _GetLoadStats();
        # Trace the fact of the load.
        Trace("Loading table $relationName from $fileName") if T(2);
        # Get the database handle.
        my $dbh = $self->{_dbh};
        # Get the input file size.
        my $fileSize = -s $fileName;
        # Get the relation data.
        my $relation = $self->_FindRelation($relationName);
        # Check the truncation flag.
        if ($truncateFlag) {
            Trace("Creating table $relationName") if T(2);
            # Compute the row count estimate. We take the size of the load file,
            # divide it by the estimated row size, and then multiply by 1.5 to
            # leave extra room. We postulate a minimum row count of 1000 to
            # prevent problems with incoming empty load files.
            my $rowSize = $self->EstimateRowSize($relationName);
            my $estimate = FIG::max($fileSize * 1.5 / $rowSize, 1000);
            # Re-create the table without its index.
            $self->CreateTable($relationName, 0, $estimate);
            # If this is a pre-index DBMS, create the index here.
            if ($dbh->{_preIndex}) {
                eval {
                    $self->CreateIndex($relationName);
                };
                if ($@) {
                    $retVal->AddMessage($@);
                }
            }
        }
        # Load the table.
        my $rv;
        eval {
            $rv = $dbh->load_table(file => $fileName, tbl => $relationName);
        };
        if (!defined $rv) {
            $retVal->AddMessage($@) if ($@);
            $retVal->AddMessage("Table load failed for $relationName using $fileName.");
            Trace("Table load failed for $relationName.") if T(1);
        } else {
            # Here we successfully loaded the table.
            $retVal->Add("tables");
            my $size = -s $fileName;
            Trace("$size bytes loaded into $relationName.") if T(2);
            # If we're rebuilding, we need to create the table indexes.
            if ($truncateFlag && ! $dbh->{_preIndex}) {
                eval {
                    $self->CreateIndex($relationName);
                };
                if ($@) {
                    $retVal->AddMessage($@);
                }
            }
        }
        # Analyze the table to improve performance.
        $dbh->vacuum_it($relationName);
        # Return the statistics.
        return $retVal;
    }
    
    =head3 GenerateEntity
    
    C<< my $fieldHash = $erdb->GenerateEntity($id, $type, \%values); >>
    
    Generate the data for a new entity instance. This method creates a field hash suitable for
    passing as a parameter to L</InsertObject>. The ID is specified by the callr, but the rest
    of the fields are generated using information in the database schema.
    
    Each data type has a default algorithm for generating random test data. This can be overridden
    by including a B<DataGen> element in the field. If this happens, the content of the element is
    executed as a PERL program in the context of this module. The element may make use of a C<$this>
    variable which contains the field hash as it has been built up to the current point. If any
    fields are dependent on other fields, the C<pass> attribute can be used to control the order
    in which the fields are generated. A field with a high data pass number will be generated after
    a field with a lower one. If any external values are needed, they should be passed in via the
    optional third parameter, which will be available to the data generation script under the name
    C<$value>. Several useful utility methods are provided for generating random values, including
    L</IntGen>, L</StringGen>, L</FloatGen>, and L</DateGen>. Note that dates are stored and generated
    in the form of a timestamp number rather than a string.
    
    =over 4
    
    =item id
    
    ID to assign to the new entity.
    
    =item type
    
    Type name for the new entity.
    
    =item values
    
    Hash containing additional values that might be needed by the data generation methods (optional).
    
    =back
    
    =cut
    
    sub GenerateEntity {
        # Get the parameters.
        my ($self, $id, $type, $values) = @_;
        # Create the return hash.
        my $this = { id => $id };
        # Get the metadata structure.
        my $metadata = $self->{_metaData};
        # Get this entity's list of fields.
        if (!exists $metadata->{Entities}->{$type}) {
            Confess("Unrecognized entity type $type in GenerateEntity.");
        } else {
            my $entity = $metadata->{Entities}->{$type};
            my $fields = $entity->{Fields};
            # Generate data from the fields.
            _GenerateFields($this, $fields, $type, $values);
        }
        # Return the hash created.
        return $this;
    }
    
    =head3 GetEntity
    
    C<< my $entityObject = $erdb->GetEntity($entityType, $ID); >>
    
    Return an object describing the entity instance with a specified ID.
    
    =over 4
    
    =item entityType
    
    Entity type name.
    
    =item ID
    
    ID of the desired entity.
    
    =item RETURN
    
    Returns a B<DBObject> representing the desired entity instance, or an undefined value if no
    instance is found with the specified key.
    
    =back
    
    =cut
    
    sub GetEntity {
        # Get the parameters.
        my ($self, $entityType, $ID) = @_;
        # Create a query.
        my $query = $self->Get([$entityType], "$entityType(id) = ?", [$ID]);
        # Get the first (and only) object.
        my $retVal = $query->Fetch();
        # Return the result.
        return $retVal;
    }
    
    =head3 GetEntityValues
    
    C<< my @values = $erdb->GetEntityValues($entityType, $ID, \@fields); >>
    
    Return a list of values from a specified entity instance.
    
    =over 4
    
    =item entityType
    
    Entity type name.
    
    =item ID
    
    ID of the desired entity.
    
    =item fields
    
    List of field names, each of the form I<objectName>C<(>I<fieldName>C<)>.
    
    =item RETURN
    
    Returns a flattened list of the values of the specified fields for the specified entity.
    
    =back
    
    =cut
    
    sub GetEntityValues {
        # Get the parameters.
        my ($self, $entityType, $ID, $fields) = @_;
        # Get the specified entity.
        my $entity = $self->GetEntity($entityType, $ID);
        # Declare the return list.
        my @retVal = ();
        # If we found the entity, push the values into the return list.
        if ($entity) {
            push @retVal, $entity->Values($fields);
        }
        # Return the result.
        return @retVal;
    }
    
    =head3 GetAll
    
    C<< my @list = $erdb->GetAll(\@objectNames, $filterClause, \@parameters, \@fields, $count); >>
    
    Return a list of values taken from the objects returned by a query. The first three
    parameters correspond to the parameters of the L</Get> method. The final parameter is
    a list of the fields desired from each record found by the query. The field name
    syntax is the standard syntax used for fields in the B<ERDB> system--
    B<I<objectName>(I<fieldName>)>-- where I<objectName> is the name of the relevant entity
    or relationship and I<fieldName> is the name of the field.
    
    The list returned will be a list of lists. Each element of the list will contain
    the values returned for the fields specified in the fourth parameter. If one of the
    fields specified returns multiple values, they are flattened in with the rest. For
    example, the following call will return a list of the features in a particular
    spreadsheet cell, and each feature will be represented by a list containing the
    feature ID followed by all of its aliases.
    
    C<< $query = $erdb->Get(['ContainsFeature', 'Feature'], "ContainsFeature(from-link) = ?", [$ssCellID], ['Feature(id)', 'Feature(alias)']); >>
    
    =over 4
    
    =item objectNames
    
    List containing the names of the entity and relationship objects to be retrieved.
    
    =item filterClause
    
    WHERE/ORDER BY clause (without the WHERE) to be used to filter and sort the query. The WHERE clause can
    be parameterized with parameter markers (C<?>). Each field used must be specified in the standard form
    B<I<objectName>(I<fieldName>)>. Any parameters specified in the filter clause should be added to the
    parameter list as additional parameters. The fields in a filter clause can come from primary
    entity relations, relationship relations, or secondary entity relations; however, all of the
    entities and relationships involved must be included in the list of object names.
    
    =item parameterList
    
    List of the parameters to be substituted in for the parameters marks in the filter clause.
    
    =item fields
    
    List of the fields to be returned in each element of the list returned.
    
    =item count
    
    Maximum number of records to return. If omitted or 0, all available records will be returned.
    
    =item RETURN
    
    Returns a list of list references. Each element of the return list contains the values for the
    fields specified in the B<fields> parameter.
    
    =back
    
    =cut
    #: Return Type @@;
    sub GetAll {
        # Get the parameters.
        my ($self, $objectNames, $filterClause, $parameterList, $fields, $count) = @_;
        # Translate the parameters from a list reference to a list. If the parameter
        # list is a scalar we convert it into a singleton list.
        my @parmList = ();
        if (ref $parameterList eq "ARRAY") {
            Trace("GetAll parm list is an array.") if T(4);
            @parmList = @{$parameterList};
        } else {
            Trace("GetAll parm list is a scalar: $parameterList.") if T(4);
            push @parmList, $parameterList;
        }
        # Insure the counter has a value.
        if (!defined $count) {
            $count = 0;
        }
        # Add the row limit to the filter clause.
        if ($count > 0) {
            $filterClause .= " LIMIT $count";
        }
        # Create the query.
        my $query = $self->Get($objectNames, $filterClause, \@parmList);
        # Set up a counter of the number of records read.
        my $fetched = 0;
        # Loop through the records returned, extracting the fields. Note that if the
        # counter is non-zero, we stop when the number of records read hits the count.
        my @retVal = ();
        while (($count == 0 || $fetched < $count) && (my $row = $query->Fetch())) {
            my @rowData = $row->Values($fields);
            push @retVal, \@rowData;
            $fetched++;
        }
        Trace("$fetched rows returned in GetAll.") if T(SQL => 4);
        # Return the resulting list.
        return @retVal;
    }
    
    =head3 EstimateRowSize
    
    C<< my $rowSize = $erdb->EstimateRowSize($relName); >>
    
    Estimate the row size of the specified relation. The estimated row size is computed by adding
    up the average length for each data type.
    
    =over 4
    
    =item relName
    
    Name of the relation whose estimated row size is desired.
    
    =item RETURN
    
    Returns an estimate of the row size for the specified relation.
    
    =back
    
    =cut
    #: Return Type $;
    sub EstimateRowSize {
        # Get the parameters.
        my ($self, $relName) = @_;
        # Declare the return variable.
        my $retVal = 0;
        # Find the relation descriptor.
        my $relation = $self->_FindRelation($relName);
        # Get the list of fields.
        for my $fieldData (@{$relation->{Fields}}) {
            # Get the field type and add its length.
            my $fieldLen = $TypeTable{$fieldData->{type}}->{avgLen};
            $retVal += $fieldLen;
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 GetFieldTable
    
    C<< my $fieldHash = $self->GetFieldTable($objectnName); >>
    
    Get the field structure for a specified entity or relationship.
    
    =over 4
    
    =item objectName
    
    Name of the desired entity or relationship.
    
    =item RETURN
    
    The table containing the field descriptors for the specified object.
    
    =back
    
    =cut
    
    sub GetFieldTable {
        # Get the parameters.
        my ($self, $objectName) = @_;
        # Get the descriptor from the metadata.
        my $objectData = $self->_GetStructure($objectName);
        # Return the object's field table.
        return $objectData->{Fields};
    }
    
    =head2 Data Mining Methods
    
    =head3 GetUsefulCrossValues
    
    C<< my @attrNames = $sprout->GetUsefulCrossValues($sourceEntity, $relationship); >>
    
    Return a list of the useful attributes that would be returned by a B<Cross> call
    from an entity of the source entity type through the specified relationship. This
    means it will return the fields of the target entity type and the intersection data
    fields in the relationship. Only primary table fields are returned. In other words,
    the field names returned will be for fields where there is always one and only one
    value.
    
    =over 4
    
    =item sourceEntity
    
    Name of the entity from which the relationship crossing will start.
    
    =item relationship
    
    Name of the relationship being crossed.
    
    =item RETURN
    
    Returns a list of field names in Sprout field format (I<objectName>C<(>I<fieldName>C<)>.
    
    =back
    
    =cut
    #: Return Type @;
    sub GetUsefulCrossValues {
        # Get the parameters.
        my ($self, $sourceEntity, $relationship) = @_;
        # Declare the return variable.
        my @retVal = ();
        # Determine the target entity for the relationship. This is whichever entity is not
        # the source entity. So, if the source entity is the FROM, we'll get the name of
        # the TO, and vice versa.
        my $relStructure = $self->_GetStructure($relationship);
        my $targetEntityType = ($relStructure->{from} eq $sourceEntity ? "to" : "from");
        my $targetEntity = $relStructure->{$targetEntityType};
        # Get the field table for the entity.
        my $entityFields = $self->GetFieldTable($targetEntity);
        # The field table is a hash. The hash key is the field name. The hash value is a structure.
        # For the entity fields, the key aspect of the target structure is that the {relation} value
        # must match the entity name.
        my @fieldList = map { "$targetEntity($_)" } grep { $entityFields->{$_}->{relation} eq $targetEntity }
                            keys %{$entityFields};
        # Push the fields found onto the return variable.
        push @retVal, sort @fieldList;
        # Get the field table for the relationship.
        my $relationshipFields = $self->GetFieldTable($relationship);
        # Here we have a different rule. We want all the fields other than "from-link" and "to-link".
        # This may end up being an empty set.
        my @fieldList2 = map { "$relationship($_)" } grep { $_ ne "from-link" && $_ ne "to-link" }
                            keys %{$relationshipFields};
        # Push these onto the return list.
        push @retVal, sort @fieldList2;
        # Return the result.
        return @retVal;
    }
    
    =head3 FindColumn
    
    C<< my $colIndex = ERDB::FindColumn($headerLine, $columnIdentifier); >>
    
    Return the location a desired column in a data mining header line. The data
    mining header line is a tab-separated list of column names. The column
    identifier is either the numerical index of a column or the actual column
    name.
    
    =over 4
    
    =item headerLine
    
    The header line from a data mining command, which consists of a tab-separated
    list of column names.
    
    =item columnIdentifier
    
    Either the ordinal number of the desired column (1-based), or the name of the
    desired column.
    
    =item RETURN
    
    Returns the array index (0-based) of the desired column.
    
    =back
    
    =cut
    
    sub FindColumn {
        # Get the parameters.
        my ($headerLine, $columnIdentifier) = @_;
        # Declare the return variable.
        my $retVal;
        # Split the header line into column names.
        my @headers = ParseColumns($headerLine);
        # Determine whether we have a number or a name.
        if ($columnIdentifier =~ /^\d+$/) {
            # Here we have a number. Subtract 1 and validate the result.
            $retVal = $columnIdentifier - 1;
            if ($retVal < 0 || $retVal > $#headers) {
                Confess("Invalid column identifer \"$columnIdentifier\": value out of range.");
            }
        } else {
            # Here we have a name. We need to find it in the list.
            for (my $i = 0; $i <= $#headers && ! defined($retVal); $i++) {
                if ($headers[$i] eq $columnIdentifier) {
                    $retVal = $i;
                }
            }
            if (! defined($retVal)) {
                Confess("Invalid column identifier \"$columnIdentifier\": value not found.");
            }
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 ParseColumns
    
    C<< my @columns = ERDB::ParseColumns($line); >>
    
    Convert the specified data line to a list of columns.
    
    =over 4
    
    =item line
    
    A data mining input, consisting of a tab-separated list of columns terminated by a
    new-line.
    
    =item RETURN
    
    Returns a list consisting of the column values.
    
    =back
    
    =cut
    
    sub ParseColumns {
        # Get the parameters.
        my ($line) = @_;
        # Chop off the line-end.
        chomp $line;
        # Split it into a list.
        my @retVal = split(/\t/, $line);
        # Return the result.
        return @retVal;
    }
    
    =head2 Internal Utility Methods
    
    =head3 SetupSQL
    
    Process a list of object names and a filter clause so that they can be used to
    build an SQL statement. This method takes in a reference to a list of object names
    and a filter clause. It will return a corrected filter clause, a list of mapped
    names and the mapped name hash.
    
    This is an instance method.
    
    =over 4
    
    =item objectNames
    
    Reference to a list of the object names to be included in the query.
    
    =item filterClause
    
    A string containing the WHERE clause for the query (without the C<WHERE>) and also
    optionally the C<ORDER BY> and C<LIMIT> clauses.
    
    =item RETURN
    
    Returns a three-element list. The first element is the SQL statement suffix, beginning
    with the FROM clause. The second element is a reference to a list of the names to be
    used in retrieving the fields. The third element is a hash mapping the names to the
    objects they represent.
    
    =back
    
    =cut
    
    sub _SetupSQL {
        my ($self, $objectNames, $filterClause) = @_;
        # Adjust the list of object names to account for multiple occurrences of the
        # same object. We start with a hash table keyed on object name that will
        # return the object suffix. The first time an object is encountered it will
        # not be found in the hash. The next time the hash will map the object name
        # to 2, then 3, and so forth.
        my %objectHash = ();
        # This list will contain the object names as they are to appear in the
        # FROM list.
        my @fromList = ();
        # This list contains the suffixed object name for each object. It is exactly
        # parallel to the list in the $objectNames parameter.
        my @mappedNameList = ();
        # Finally, this hash translates from a mapped name to its original object name.
        my %mappedNameHash = ();
        # Now we create the lists. Note that for every single name we push something into
        # @fromList and @mappedNameList. This insures that those two arrays are exactly
        # parallel to $objectNames.
        for my $objectName (@{$objectNames}) {
            # Get the next suffix for this object.
            my $suffix = $objectHash{$objectName};
            if (! $suffix) {
                # Here we are seeing the object for the first time. The object name
                # is used as is.
                push @mappedNameList, $objectName;
                push @fromList, $objectName;
                $mappedNameHash{$objectName} = $objectName;
                # Denote the next suffix will be 2.
                $objectHash{$objectName} = 2;
            } else {
                # Here we've seen the object before. We construct a new name using
                # the suffix from the hash and update the hash.
                my $mappedName = "$objectName$suffix";
                $objectHash{$objectName} = $suffix + 1;
                # The FROM list has the object name followed by the mapped name. This
                # tells SQL it's still the same table, but we're using a different name
                # for it to avoid confusion.
                push @fromList, "$objectName $mappedName";
                # The mapped-name list contains the real mapped name.
                push @mappedNameList, $mappedName;
                # Finally, enable us to get back from the mapped name to the object name.
                $mappedNameHash{$mappedName} = $objectName;
            }
        }
        # Begin the SELECT suffix. It starts with
        #
        # FROM name1, name2, ... nameN
        #
        my $suffix = "FROM " . join(', ', @fromList);
        # Check for a filter clause.
        if ($filterClause) {
            # Here we have one, so we convert its field names and add it to the query. First,
            # We create a copy of the filter string we can work with.
            my $filterString = $filterClause;
            # Next, we sort the object names by length. This helps protect us from finding
            # object names inside other object names when we're doing our search and replace.
            my @sortedNames = sort { length($b) - length($a) } @mappedNameList;
            # We will also keep a list of conditions to add to the WHERE clause in order to link
            # entities and relationships as well as primary relations to secondary ones.
            my @joinWhere = ();
            # The final preparatory step is to create a hash table of relation names. The
            # table begins with the relation names already in the SELECT command. We may
            # need to add relations later if there is filtering on a field in a secondary
            # relation. The secondary relations are the ones that contain multiply-
            # occurring or optional fields.
            my %fromNames = map { $_ => 1 } @sortedNames;
            # We are ready to begin. We loop through the object names, replacing each
            # object name's field references by the corresponding SQL field reference.
            # Along the way, if we find a secondary relation, we will need to add it
            # to the FROM clause.
            for my $mappedName (@sortedNames) {
                # Get the length of the object name plus 2. This is the value we add to the
                # size of the field name to determine the size of the field reference as a
                # whole.
                my $nameLength = 2 + length $mappedName;
                # Get the real object name for this mapped name.
                my $objectName = $mappedNameHash{$mappedName};
                Trace("Processing $mappedName for object $objectName.") if T(4);
                # Get the object's field list.
                my $fieldList = $self->GetFieldTable($objectName);
                # Find the field references for this object.
                while ($filterString =~ m/$mappedName\(([^)]*)\)/g) {
                    # At this point, $1 contains the field name, and the current position
                    # is set immediately after the final parenthesis. We pull out the name of
                    # the field and the position and length of the field reference as a whole.
                    my $fieldName = $1;
                    my $len = $nameLength + length $fieldName;
                    my $pos = pos($filterString) - $len;
                    # Insure the field exists.
                    if (!exists $fieldList->{$fieldName}) {
                        Confess("Field $fieldName not found for object $objectName.");
                    } else {
                        Trace("Processing $fieldName at position $pos.") if T(4);
                        # Get the field's relation.
                        my $relationName = $fieldList->{$fieldName}->{relation};
                        # Now we have a secondary relation. We need to insure it matches the
                        # mapped name of the primary relation. First we peel off the suffix
                        # from the mapped name.
                        my $mappingSuffix = substr $mappedName, length($objectName);
                        # Put the mapping suffix onto the relation name to get the
                        # mapped relation name.
                        my $mappedRelationName = "$relationName$mappingSuffix";
                        # Insure the relation is in the FROM clause.
                        if (!exists $fromNames{$mappedRelationName}) {
                            # Add the relation to the FROM clause.
                            if ($mappedRelationName eq $relationName) {
                                # The name is un-mapped, so we add it without
                                # any frills.
                                $suffix .= ", $relationName";
                                push @joinWhere, "$objectName.id = $relationName.id";
                            } else {
                                # Here we have a mapping situation.
                                $suffix .= ", $relationName $mappedRelationName";
                                push @joinWhere, "$mappedRelationName.id = $mappedName.id";
                            }
                            # Denote we have this relation available for future fields.
                            $fromNames{$mappedRelationName} = 1;
                        }
                        # Form an SQL field reference from the relation name and the field name.
                        my $sqlReference = "$mappedRelationName." . _FixName($fieldName);
                        # Put it into the filter string in place of the old value.
                        substr($filterString, $pos, $len) = $sqlReference;
                        # Reposition the search.
                        pos $filterString = $pos + length $sqlReference;
                    }
                }
            }
            # The next step is to join the objects together. We only need to do this if there
            # is more than one object in the object list. We start with the first object and
            # run through the objects after it. Note also that we make a safety copy of the
            # list before running through it.
            my @mappedObjectList = @mappedNameList;
            my $lastMappedObject = shift @mappedObjectList;
            # Get the join table.
            my $joinTable = $self->{_metaData}->{Joins};
            # Loop through the object list.
            for my $thisMappedObject (@mappedObjectList) {
                # Look for a join using the real object names.
                my $lastObject = $mappedNameHash{$lastMappedObject};
                my $thisObject = $mappedNameHash{$thisMappedObject};
                my $joinKey = "$lastObject/$thisObject";
                if (!exists $joinTable->{$joinKey}) {
                    # Here there's no join, so we throw an error.
                    Confess("No join exists to connect from $lastMappedObject to $thisMappedObject.");
                } else {
                    # Get the join clause.
                    my $unMappedJoin = $joinTable->{$joinKey};
                    # Fix the names.
                    $unMappedJoin =~ s/$lastObject/$lastMappedObject/;
                    $unMappedJoin =~ s/$thisObject/$thisMappedObject/;
                    push @joinWhere, $unMappedJoin;
                    # Save this object as the last object for the next iteration.
                    $lastMappedObject = $thisMappedObject;
                }
            }
            # Now we need to handle the whole ORDER BY / LIMIT thing. The important part
            # here is we want the filter clause to be empty if there's no WHERE filter.
            # We'll put the ORDER BY / LIMIT clauses in the following variable.
            my $orderClause = "";
            # Locate the ORDER BY or LIMIT verbs (if any). We use a non-greedy
            # operator so that we find the first occurrence of either verb.
            if ($filterString =~ m/^(.*?)\s*(ORDER BY|LIMIT)/g) {
                # Here we have an ORDER BY or LIMIT verb. Split it off of the filter string.
                my $pos = pos $filterString;
                $orderClause = $2 . substr($filterString, $pos);
                $filterString = $1;
            }
            # Add the filter and the join clauses (if any) to the SELECT command.
            if ($filterString) {
                Trace("Filter string is \"$filterString\".") if T(4);
                push @joinWhere, "($filterString)";
            }
            if (@joinWhere) {
                $suffix .= " WHERE " . join(' AND ', @joinWhere);
            }
            # Add the sort or limit clause (if any) to the SELECT command.
            if ($orderClause) {
                $suffix .= " $orderClause";
            }
        }
        # Return the suffix, the mapped name list, and the mapped name hash.
        return ($suffix, \@mappedNameList, \%mappedNameHash);
    }
    
    =head3 GetStatementHandle
    
    This method will prepare and execute an SQL query, returning the statement handle.
    The main reason for doing this here is so that everybody who does SQL queries gets
    the benefit of tracing.
    
    This is an instance method.
    
    =over 4
    
    =item command
    
    Command to prepare and execute.
    
    =item params
    
    Reference to a list of the values to be substituted in for the parameter marks.
    
    =item RETURN
    
    Returns a prepared and executed statement handle from which the caller can extract
    results.
    
    =back
    
    =cut
    
    sub _GetStatementHandle {
        # Get the parameters.
        my ($self, $command, $params) = @_;
        # Trace the query.
        Trace("SQL query: $command") if T(SQL => 3);
        Trace("PARMS: '" . (join "', '", @{$params}) . "'") if (T(SQL => 4) && (@{$params} > 0));
        # Get the database handle.
        my $dbh = $self->{_dbh};
        # Prepare the command.
        my $sth = $dbh->prepare_command($command);
        # Execute it with the parameters bound in.
        $sth->execute(@{$params}) || Confess("SELECT error" . $sth->errstr());
        # Return the statement handle.
        return $sth;
    }
    
    =head3 GetLoadStats
    
    Return a blank statistics object for use by the load methods.
    
    This is a static method.
    
    =cut
    
    sub _GetLoadStats{ 
        return Stats->new();
    }
    
    =head3 GenerateFields
    
    Generate field values from a field structure and store in a specified table. The field names
    are first sorted by pass count, certain pre-defined fields are removed from the list, and
    then we rip through them evaluation the data generation string. Fields in the primary relation
    are stored as scalars; fields in secondary relations are stored as value lists.
    
    This is a static method.
    
    =over 4
    
    =item this
    
    Hash table into which the field values should be placed.
    
    =item fields
    
    Field structure from which the field descriptors should be taken.
    
    =item type
    
    Type name of the object whose fields are being generated.
    
    =item values (optional)
    
    Reference to a value structure from which additional values can be taken.
    
    =item from (optiona)
    
    Reference to the source entity instance if relationship data is being generated.
    
    =item to (optional)
    
    Reference to the target entity instance if relationship data is being generated.
    
    =back
    
    =cut
    
    sub _GenerateFields {
        # Get the parameters.
        my ($this, $fields, $type, $values, $from, $to) = @_;
        # Sort the field names by pass number.
        my @fieldNames = sort { $fields->{$a}->{DataGen}->{pass} <=> $fields->{$b}->{DataGen}->{pass} } keys %{$fields};
        # Loop through the field names, generating data.
        for my $name (@fieldNames) {
            # Only proceed if this field needs to be generated.
            if (!exists $this->{$name}) {
                # Get this field's data generation descriptor.
                my $fieldDescriptor = $fields->{$name};
                my $data = $fieldDescriptor->{DataGen};
                # Get the code to generate the field value.
                my $codeString = $data->{content};
                # Determine whether or not this field is in the primary relation.
                if ($fieldDescriptor->{relation} eq $type) {
                    # Here we have a primary relation field. Store the field value as
                    # a scalar.
                    $this->{$name} = eval($codeString);
                } else {
                    # Here we have a secondary relation field. Create a null list
                    # and push the desired number of field values onto it.
                    my @fieldValues = ();
                    my $count = IntGen(0,$data->{testCount});
                    for (my $i = 0; $i < $count; $i++) {
                        my $newValue = eval($codeString);
                        push @fieldValues, $newValue;
                    }
                    # Store the value list in the main hash.
                    $this->{$name} = \@fieldValues;
                }
            }
        }
    }
    
    =head3 DumpRelation
    
    Dump the specified relation's to the specified output file in tab-delimited format.
    
    This is an instance method.
    
    =over 4
    
    =item outputDirectory
    
    Directory to contain the output file.
    
    =item relationName
    
    Name of the relation to dump.
    
    =item relation
    
    Structure describing the relation to be dumped.
    
    =back
    
    =cut
    
    sub _DumpRelation {
        # Get the parameters.
        my ($self, $outputDirectory, $relationName, $relation) = @_;
        # Open the output file.
        my $fileName = "$outputDirectory/$relationName.dtx";
        open(DTXOUT, ">$fileName") || Confess("Could not open dump file $fileName: $!");
        # Create a query for the specified relation.
        my $dbh = $self->{_dbh};
        my $query = $dbh->prepare_command("SELECT * FROM $relationName");
        # Execute the query.
        $query->execute() || Confess("SELECT error dumping $relationName.");
        # Loop through the results.
        while (my @row = $query->fetchrow) {
            # Escape any tabs or new-lines in the row text.
            for my $field (@row) {
                $field =~ s/\n/\\n/g;
                $field =~ s/\t/\\t/g;
            }
            # Tab-join the row and write it to the output file.
            my $rowText = join("\t", @row);
            print DTXOUT "$rowText\n";
        }
        # Close the output file.
        close DTXOUT;
    }
    
    =head3 GetStructure
    
    Get the data structure for a specified entity or relationship.
    
    This is an instance method.
    
    =over 4
    
    =item objectName
    
    Name of the desired entity or relationship.
    
    =item RETURN
    
    The descriptor for the specified object.
    
    =back
    
    =cut
    
    sub _GetStructure {
        # Get the parameters.
        my ($self, $objectName) = @_;
        # Get the metadata structure.
        my $metadata = $self->{_metaData};
        # Declare the variable to receive the descriptor.
        my $retVal;
        # Get the descriptor from the metadata.
        if (exists $metadata->{Entities}->{$objectName}) {
            $retVal = $metadata->{Entities}->{$objectName};
        } elsif (exists $metadata->{Relationships}->{$objectName}) {
            $retVal = $metadata->{Relationships}->{$objectName};
        } else {
            Confess("Object $objectName not found in database.");
        }
        # Return the descriptor.
        return $retVal;
    }
    
    =head3 GetRelationTable
    
    Get the list of relations for a specified entity or relationship.
    
    This is an instance method.
    
    =over 4
    
    =item objectName
    
    Name of the desired entity or relationship.
    
    =item RETURN
    
    A table containing the relations for the specified object.
    
    =back
    
    =cut
    
    sub _GetRelationTable {
        # Get the parameters.
        my ($self, $objectName) = @_;
        # Get the descriptor from the metadata.
        my $objectData = $self->_GetStructure($objectName);
        # Return the object's relation list.
        return $objectData->{Relations};
    }
    
    =head3 ValidateFieldNames
    
    Determine whether or not the field names are valid. A description of the problems with the names
    will be written to the standard error output. If there is an error, this method will abort. This is
    a static method.
    
    =over 4
    
    =item metadata
    
    Metadata structure loaded from the XML data definition.
    
    =back
    
    =cut
    
    sub _ValidateFieldNames {
        # Get the object.
        my ($metadata) = @_;
        # Declare the return value. We assume success.
        my $retVal = 1;
        # Loop through the sections of the database definition.
        for my $section ('Entities', 'Relationships') {
            # Loop through the objects in this section.
            for my $object (values %{$metadata->{$section}}) {
                # Loop through the object's fields.
                for my $fieldName (keys %{$object->{Fields}}) {
                    # Now we make some initial validations.
                    if ($fieldName =~ /--/) {
                        # Here we have a doubled minus sign.
                        print STDERR "Field name $fieldName has a doubled hyphen.\n";
                        $retVal = 0;
                    } elsif ($fieldName !~ /^[A-Za-z]/) {
                        # Here the field name is missing the initial letter.
                        print STDERR "Field name $fieldName does not begin with a letter.\n";
                        $retVal = 0;
                    } else {
                        # Strip out the minus signs. Everything remaining must be a letter
                        # or digit.
                        my $strippedName = $fieldName;
                        $strippedName =~ s/-//g;
                        if ($strippedName !~ /^[A-Za-z0-9]+$/) {
                            print STDERR "Field name $fieldName contains illegal characters.\n";
                            $retVal = 0;
                        }
                    }
                }
            }
        }
        # If an error was found, fail.
        if ($retVal  == 0) {
            Confess("Errors found in field names.");
        }
    }
    
    =head3 LoadRelation
    
    Load a relation from the data in a tab-delimited disk file. The load will only take place if a disk
    file with the same name as the relation exists in the specified directory.
    
    This is an instance method.
    
    =over 4
    
    =item dbh
    
    DBKernel object for accessing the database.
    
    =item directoryName
    
    Name of the directory containing the tab-delimited data files.
    
    =item relationName
    
    Name of the relation to load.
    
    =item rebuild
    
    TRUE if the table should be dropped and re-created before loading.
    
    =item RETURN
    
    Returns a statistical object describing the number of records read and a list of error messages.
    
    =back
    
    =cut
    
    sub _LoadRelation {
        # Get the parameters.
        my ($self, $directoryName, $relationName, $rebuild) = @_;
        # Create the file name.
        my $fileName = "$directoryName/$relationName";
        # If the file doesn't exist, try adding the .dtx suffix.
        if (! -e $fileName) {
            $fileName .= ".dtx";
            if (! -e $fileName) {
                $fileName = "";
            }
        }
        # Create the return object.
        my $retVal = _GetLoadStats();
        # If a file exists to load the table, its name will be in $fileName. Otherwise, $fileName will
        # be a null string.
        if ($fileName ne "") {
            # Load the relation from the file.
            $retVal = $self->LoadTable($fileName, $relationName, $rebuild);
        } elsif ($rebuild) {
            # Here we are rebuilding, but no file exists, so we just re-create the table.
            $self->CreateTable($relationName, 1);
        }
        # Return the statistics from the load.
        return $retVal;
    }
    
    =head3 LoadMetaData
    
    This method loads the data describing this database from an XML file into a metadata structure.
    The resulting structure is a set of nested hash tables containing all the information needed to
    load or use the database. The schema for the XML file is F<ERDatabase.xml>.
    
    This is a static method.
    
    =over 4
    
    =item filename
    
    Name of the file containing the database definition.
    
    =item RETURN
    
    Returns a structure describing the database.
    
    =back
    
    =cut
    
    sub _LoadMetaData {
        # Get the parameters.
        my ($filename) = @_;
        Trace("Reading Sprout DBD from $filename.") if T(2);
        # Slurp the XML file into a variable. Extensive use of options is used to insure we
        # get the exact structure we want.
        my $metadata = XML::Simple::XMLin($filename,
                                          GroupTags => { Relationships => 'Relationship',
                                                         Entities => 'Entity',
                                                         Fields => 'Field',
                                                         Indexes => 'Index',
                                                         IndexFields => 'IndexField'},
                                          KeyAttr => { Relationship => 'name',
                                                       Entity => 'name',
                                                       Field => 'name'},
                                          ForceArray => ['Field', 'Index', 'IndexField'],
                                          ForceContent => 1,
                                          NormalizeSpace => 2
                                          );
        Trace("XML metadata loaded from file $filename.") if T(1);
        # Before we go any farther, we need to validate the field and object names. If an error is found,
        # the method below will fail.
        _ValidateFieldNames($metadata);
        # Next we need to create a hash table for finding relations. The entities and relationships are
        # implemented as one or more database relations.
        my %masterRelationTable = ();
        # Loop through the entities.
        my $entityList = $metadata->{Entities};
        for my $entityName (keys %{$entityList}) {
            my $entityStructure = $entityList->{$entityName};
            #
            # The first step is to create all the entity's default values. For C<Field> elements,
            # the relation name must be added where it is not specified. For relationships,
            # the B<from-link> and B<to-link> fields must be inserted, and for entities an B<id>
            # field must be added to each relation. Finally, each field will have a C<PrettySort> attribute
            # added that can be used to pull the implicit fields to the top when displaying the field
            # documentation. The PrettySort values are 1-based and indicate in which pass through a
            # relation's data the field should be displayed-- 1 for the first pass, 2 for the second,
            # and so on.
            #
            # Fix up this entity.
            _FixupFields($entityStructure, $entityName, 2, 3);
            # Add the ID field.
            _AddField($entityStructure, 'id', { type => $entityStructure->{keyType},
                                                relation => $entityName,
                                                Notes => { content => "Unique identifier for this \[b\]$entityName\[/b\]." },
                                                PrettySort => 1});
            #
            # The current field list enables us to quickly find the relation containing a particular field.
            # We also need a list that tells us the fields in each relation. We do this by creating a
            # Relations structure in the entity structure and collating the fields into it based on their
            # C<relation> property. There is one tricky bit, which is that every relation has to have the
            # C<id> field in it. Note also that the field list is put into a C<Fields> member of the
            # relation's structure so that it looks more like the entity and relationship structures.
            #
            # First we need to create the relations list.
            my $relationTable = { };
            # Loop through the fields. We use a list of field names to prevent a problem with
            # the hash table cursor losing its place during the loop.
            my $fieldList = $entityStructure->{Fields};
            my @fieldNames = keys %{$fieldList};
            for my $fieldName (@fieldNames) {
                my $fieldData = $fieldList->{$fieldName};
                # Get the current field's relation name.
                my $relationName = $fieldData->{relation};
                # Insure the relation exists.
                if (!exists $relationTable->{$relationName}) {
                    $relationTable->{$relationName} = { Fields => { } };
                }
                # Add the field to the relation's field structure.
                $relationTable->{$relationName}->{Fields}->{$fieldName} = $fieldData;
            }
            # Now that we've organized all our fields by relation name we need to do some serious
            # housekeeping. We must add the C<id> field to every relation and convert each relation
            # to a list of fields. First, we need the ID field itself.
            my $idField = $fieldList->{id};
            # Loop through the relations.
            for my $relationName (keys %{$relationTable}) {
                my $relation = $relationTable->{$relationName};
                # Get the relation's field list.
                my $relationFieldList = $relation->{Fields};
                # Add the ID field to it. If the field's already there, it will not make any
                # difference.
                $relationFieldList->{id} = $idField;
                # Convert the field set from a hash into a list using the pretty-sort number.
                $relation->{Fields} = _ReOrderRelationTable($relationFieldList);
                # Add the relation to the master table.
                $masterRelationTable{$relationName} = $relation;
            }
            # The indexes come next. The primary relation will have a unique-keyed index based on the ID field.
            # The other relations must have at least one index that begins with the ID field. In addition, the
            # metadata may require alternate indexes. We do those alternate indexes first. To begin, we need to
            # get the entity's field list and index list.
            my $indexList = $entityStructure->{Indexes};
            # Loop through the indexes.
            for my $indexData (@{$indexList}) {
                # We need to find this index's fields. All of them should belong to the same relation.
                # The ID field is an exception, since it's in all relations.
                my $relationName = '0';
                for my $fieldDescriptor (@{$indexData->{IndexFields}}) {
                    # Get this field's name.
                    my $fieldName = $fieldDescriptor->{name};
                    # Only proceed if it is NOT the ID field.
                    if ($fieldName ne 'id') {
                        # Find the relation containing the current index field.
                        my $thisName = $fieldList->{$fieldName}->{relation};
                        if ($relationName eq '0') {
                            # Here we're looking at the first field, so we save its relation name.
                            $relationName = $thisName;
                        } elsif ($relationName ne $thisName) {
                            # Here we have a field mismatch.
                            Confess("Mixed index: field $fieldName does not belong to relation $relationName.");
                        }
                    }
                }
                # Now $relationName is the name of the relation that contains this index. Add the index structure
                # to the relation.
                push @{$relationTable->{$relationName}->{Indexes}}, $indexData;
            }
            # Now each index has been put in a relation. We need to add the primary index for the primary
            # relation.
            push @{$relationTable->{$entityName}->{Indexes}},
                { IndexFields => [ {name => 'id', order => 'ascending'} ], Unique => 'true',
                  Notes => { content => "Primary index for $entityName." }
                };
            # The next step is to insure that each relation has at least one index that begins with the ID field.
            # After that, we convert each relation's index list to an index table. We first need to loop through
            # the relations.
            for my $relationName (keys %{$relationTable}) {
                my $relation = $relationTable->{$relationName};
                # Get the relation's index list.
                my $indexList = $relation->{Indexes};
                # Insure this relation has an ID index.
                my $found = 0;
                for my $index (@{$indexList}) {
                    if ($index->{IndexFields}->[0]->{name} eq "id") {
                        $found = 1;
                    }
                }
                if ($found == 0) {
                    push @{$indexList}, { IndexFields => [ {name => 'id', order => 'ascending'} ] };
                }
                # Now we need to convert the relation's index list to an index table. We begin by creating
                # an empty table in the relation structure.
                $relation->{Indexes} = { };
                # Loop through the indexes.
                my $count = 0;
                for my $index (@{$indexList}) {
                    # Add this index to the index table.
                    _AddIndex("idx$relationName$count", $relation, $index);
                    # Increment the counter so that the next index has a different name.
                    $count++;
                }
            }
            # Finally, we add the relation structure to the entity.
            $entityStructure->{Relations} = $relationTable;
        }
        # Loop through the relationships. Relationships actually turn out to be much simpler than entities.
        # For one thing, there is only a single constituent relation.
        my $relationshipList = $metadata->{Relationships};
        for my $relationshipName (keys %{$relationshipList}) {
            my $relationshipStructure = $relationshipList->{$relationshipName};
            # Fix up this relationship.
            _FixupFields($relationshipStructure, $relationshipName, 2, 3);
            # Format a description for the FROM field.
            my $fromEntity = $relationshipStructure->{from};
            my $fromComment = "<b>id</b> of the source <b><a href=\"#$fromEntity\">$fromEntity</a></b>.";
            # Get the FROM entity's key type.
            my $fromType = $entityList->{$fromEntity}->{keyType};
            # Add the FROM field.
            _AddField($relationshipStructure, 'from-link', { type => $fromType,
                                                        relation => $relationshipName,
                                                        Notes => { content => $fromComment },
                                                        PrettySort => 1});
            # Format a description for the TO field.
            my $toEntity = $relationshipStructure->{to};
            my $toComment = "<b>id</b> of the target <b><a href=\"#$toEntity\">$toEntity</a></b>.";
            # Get the TO entity's key type.
            my $toType = $entityList->{$toEntity}->{keyType};
            # Add the TO field.
            _AddField($relationshipStructure, 'to-link', { type=> $toType,
                                                      relation => $relationshipName,
                                                      Notes => { content => $toComment },
                                                      PrettySort => 1});
            # Create an index-free relation from the fields.
            my $thisRelation = { Fields => _ReOrderRelationTable($relationshipStructure->{Fields}),
                                 Indexes => { } };
            $relationshipStructure->{Relations} = { $relationshipName => $thisRelation };
            # Create the FROM and TO indexes.
            _CreateRelationshipIndex("From", $relationshipName, $relationshipStructure);
            _CreateRelationshipIndex("To", $relationshipName, $relationshipStructure);
            # Add the relation to the master table.
            $masterRelationTable{$relationshipName} = $thisRelation;
        }
        # Now store the master relation table in the metadata structure.
        $metadata->{RelationTable} = \%masterRelationTable;
        # Our final task is to create the join table. The join table is a hash that describes all
        # the join clauses for traveling through the relationships. The join clause is an equality
        # condition that can be put into a WHERE clause in order to join two objects. Two relationships
        # can be joined if they share an entity in common; and an entity can be joined to a relationship
        # if the entity is at either end of the relationship.
        my %joinTable = ();
        # Loop through the entities.
        for my $entityName (keys %{$entityList}) {
            # Build three lists of the relationships connected to this entity. One will be
            # for relationships from the entity, one for relationships to the entity, and
            # one for recursive relationships.
            my @fromList = ();
            my @toList = ();
            my @bothList = ();
            Trace("Join table build for $entityName.") if T(metadata => 4);
            for my $relationshipName (keys %{$relationshipList}) {
                my $relationship = $relationshipList->{$relationshipName};
                # Determine if this relationship has our entity in one of its link fields.
                my $fromEntity = $relationship->{from};
                my $toEntity = $relationship->{to};
                Trace("Join check for relationship $relationshipName from $fromEntity to $toEntity.") if T(Joins => 4);
                if ($fromEntity eq $entityName) {
                    if ($toEntity eq $entityName) {
                        # Here the relationship is recursive.
                        push @bothList, $relationshipName;
                        Trace("Relationship $relationshipName put in both-list.") if T(metadata => 4);
                    } else {
                        # Here the relationship comes from the entity.
                        push @fromList, $relationshipName;
                        Trace("Relationship $relationshipName put in from-list.") if T(metadata => 4);
                    }
                } elsif ($toEntity eq $entityName) {
                    # Here the relationship goes to the entity.
                    push @toList, $relationshipName;
                    Trace("Relationship $relationshipName put in to-list.") if T(metadata => 4);
                }
            }
            # Create the nonrecursive joins. Note that we build two hashes for running
            # through the nonrecursive relationships since we'll have an outer loop
            # and an inner loop, and we can't do two "each" iterations on the same
            # hash table at the same time.
            my %directRelationships = ( from => \@fromList, to => \@toList );
            my %otherRelationships = ( from => \@fromList, to => \@toList );
            for my $linkType (keys %directRelationships) {
                my $relationships = $directRelationships{$linkType};
                # Loop through all the relationships.
                for my $relationshipName (@{$relationships}) {
                    # Create joins between the entity and this relationship.
                    my $linkField = "$relationshipName.${linkType}_link";
                    my $joinClause = "$entityName.id = $linkField";
                    Trace("Entity join clause is $joinClause for $entityName and $relationshipName.") if T(metadata => 4);
                    $joinTable{"$entityName/$relationshipName"} = $joinClause;
                    $joinTable{"$relationshipName/$entityName"} = $joinClause;
                    # Create joins between this relationship and the other relationships.
                    for my $otherType (keys %otherRelationships) {
                        my $otherships = $otherRelationships{$otherType};
                        for my $otherName (@{$otherships}) {
                            # Get the key for this join.
                            my $joinKey = "$otherName/$relationshipName";
                            # Check for a duplicate or a self-join.
                            if (exists $joinTable{$joinKey}) {
                                # Here we have a duplicate, which means that the join
                                # path is ambiguous. We delete the join from the join
                                # table to prevent it from being used.
                                delete $joinTable{$joinKey};
                                Trace("Deleting ambiguous join $joinKey.") if T(4);
                            } elsif ($otherName ne $relationshipName) {
                                # Here we have a valid join. Note that joins between a
                                # relationship and itself are prohibited.
                                my $relJoinClause = "$otherName.${otherType}_link = $linkField";
                                $joinTable{$joinKey} = $relJoinClause;
                                Trace("Relationship join clause is $relJoinClause for $joinKey.") if T(metadata => 4);
                            }
                        }
                    }
                    # Create joins between this relationship and the recursive relationships.
                    # We don't need to check for ambiguous joins here, because a recursive
                    # relationship can only be ambiguous with another recursive relationship,
                    # and the incoming relationship from the outer loop is never recursive.
                    for my $otherName (@bothList) {
                        Trace("Setting up relationship joins to recursive relationship $otherName with $relationshipName.") if T(metadata => 4);
                        # Join from the left.
                        $joinTable{"$relationshipName/$otherName"} =
                            "$linkField = $otherName.from_link";
                        # Join from the right.
                        $joinTable{"$otherName/$relationshipName"} =
                            "$otherName.to_link = $linkField";
                    }
                }
            }
            # Create entity joins for the recursive relationships. Unlike the non-recursive
            # joins, the direction makes a difference with the recursive joins. This can give
            # rise to situations where we can't create the path we want; however, it is always
            # possible to get the same effect using multiple queries.
            for my $relationshipName (@bothList) {
                Trace("Setting up entity joins to recursive relationship $relationshipName with $entityName.") if T(metadata => 4);
                # Join to the entity from each direction.
                $joinTable{"$entityName/$relationshipName"} =
                    "$entityName.id = $relationshipName.from_link";
                $joinTable{"$relationshipName/$entityName"} =
                    "$relationshipName.to_link = $entityName.id";
            }
        }
        # Add the join table to the structure.
        $metadata->{Joins} = \%joinTable;
        # Return the slurped and fixed-up structure.
        return $metadata;
    }
    
    =head3 SortNeeded
    
    C<< my $flag = $erdb->SortNeeded($relationName); >>
    
    Return TRUE if the specified relation should be sorted during loading to remove duplicate keys,
    else FALSE.
    
    =over 4
    
    =item relationName
    
    Name of the relation to be examined.
    
    =item RETURN
    
    Returns TRUE if the relation needs a sort, else FALSE.
    
    =back
    
    =cut
    #: Return Type $;
    sub SortNeeded {
        # Get the parameters.
        my ($self, $relationName) = @_;
        # Declare the return variable.
        my $retVal = 0;
        # Find out if the relation is a primary entity relation.
        my $entityTable = $self->{_metaData}->{Entities};
        if (exists $entityTable->{$relationName}) {
            my $keyType = $entityTable->{$relationName}->{keyType};
            Trace("Relation $relationName found in entity table with key type $keyType.") if T(3);
            # If the key is not a hash string, we must do the sort.
            if ($keyType ne 'hash-string') {
                $retVal = 1;
            }
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 CreateRelationshipIndex
    
    Create an index for a relationship's relation.
    
    This is a static method.
    
    =over 4
    
    =item indexKey
    
    Type of index: either C<"From"> or C<"To">.
    
    =item relationshipName
    
    Name of the relationship.
    
    =item relationshipStructure
    
    Structure describing the relationship that the index will sort.
    
    =back
    
    =cut
    
    sub _CreateRelationshipIndex {
        # Get the parameters.
        my ($indexKey, $relationshipName, $relationshipStructure) = @_;
        # Get the target relation.
        my $relationStructure = $relationshipStructure->{Relations}->{$relationshipName};
        # Create a descriptor for the link field that goes at the beginning of this index.
        my $firstField = { name => lcfirst $indexKey . '-link', order => 'ascending' };
        # Get the target index descriptor.
        my $newIndex = $relationshipStructure->{$indexKey . "Index"};
        # Add the first field to the index's field list. Due to the craziness of PERL, if the
        # index descriptor does not exist, it will be created automatically so we can add
        # the field to it.
        unshift @{$newIndex->{IndexFields}}, $firstField;
        # If this is a one-to-many relationship, the "To" index is unique.
        if ($relationshipStructure->{arity} eq "1M" && $indexKey eq "To") {
            $newIndex->{Unique} = 'true';
        }
        # Add the index to the relation.
        _AddIndex("idx$relationshipName$indexKey", $relationStructure, $newIndex);
    }
    
    =head3 AddIndex
    
    Add an index to a relation structure.
    
    This is a static method.
    
    =over 4
    
    =item indexName
    
    Name to give to the new index.
    
    =item relationStructure
    
    Relation structure to which the new index should be added.
    
    =item newIndex
    
    New index to add.
    
    =back
    
    =cut
    
    sub _AddIndex {
        # Get the parameters.
        my ($indexName, $relationStructure, $newIndex) = @_;
        # We want to re-do the index's field list. Instead of an object for each field,
        # we want a string consisting of the field name optionally followed by the token DESC.
        my @fieldList = ( );
        for my $field (@{$newIndex->{IndexFields}}) {
            # Create a string containing the field name.
            my $fieldString = $field->{name};
            # Add the ordering token if needed.
            if ($field->{order} eq "descending") {
                $fieldString .= " DESC";
            }
            # Push the result onto the field list.
            push @fieldList, $fieldString;
        }
        # Store the field list just created as the new index field list.
        $newIndex->{IndexFields} = \@fieldList;
        # Add the index to the relation's index list.
        $relationStructure->{Indexes}->{$indexName} = $newIndex;
    }
    
    =head3 FixupFields
    
    This method fixes the field list for an entity or relationship. It will add the caller-specified
    relation name to fields that do not have a name and set the C<PrettySort> value as specified.
    
    This is a static method.
    
    =over 4
    
    =item structure
    
    Entity or relationship structure to be fixed up.
    
    =item defaultRelationName
    
    Default relation name to be added to the fields.
    
    =item prettySortValue
    
    C<PrettySort> value for the relation's normal fields.
    
    =item textPrettySortValue
    
    C<PrettySort> value for the relation's text fields. This value can be set to one greater than the
    normal pretty sort value so that text fields go at the end of each relation.
    
    =back
    
    =cut
    
    sub _FixupFields {
        # Get the parameters.
        my ($structure, $defaultRelationName, $prettySortValue, $textPrettySortValue) = @_;
        # Insure the structure has a field list.
        if (!exists $structure->{Fields}) {
            # Here it doesn't, so we create a new one.
            $structure->{Fields} = { };
        } else {
            # Here we have a field list. Loop through its fields.
            my $fieldStructures = $structure->{Fields};
            for my $fieldName (keys %{$fieldStructures}) {
                Trace("Processing field $fieldName of $defaultRelationName.") if T(4);
                my $fieldData = $fieldStructures->{$fieldName};
                # Get the field type.
                my $type = $fieldData->{type};
                # Plug in a relation name if it is needed.
                Tracer::MergeOptions($fieldData, { relation => $defaultRelationName });
                # Plug in a data generator if we need one.
                if (!exists $fieldData->{DataGen}) {
                    # The data generator will use the default for the field's type.
                    $fieldData->{DataGen} = { content => $TypeTable{$type}->{dataGen} };
                }
                # Plug in the defaults for the optional data generation parameters.
                Tracer::MergeOptions($fieldData->{DataGen}, { testCount => 1, pass => 0 });
                # Add the PrettySortValue.
                $fieldData->{PrettySort} = (($type eq "text") ? $textPrettySortValue : $prettySortValue);
            }
        }
    }
    
    =head3 FixName
    
    Fix the incoming field name so that it is a legal SQL column name.
    
    This is a static method.
    
    =over 4
    
    =item fieldName
    
    Field name to fix.
    
    =item RETURN
    
    Returns the fixed-up field name.
    
    =back
    
    =cut
    
    sub _FixName {
        # Get the parameter.
        my ($fieldName) = @_;
        # Replace its minus signs with underscores.
        $fieldName =~ s/-/_/g;
        # Return the result.
        return $fieldName;
    }
    
    =head3 FixNames
    
    Fix all the field names in a list.
    
    This is a static method.
    
    =over 4
    
    =item field1, field2, field3, ... fieldn
    
    List of field names to fix.
    
    =item RETURN
    
    Returns a list of fixed-up versions of the incoming field names.
    
    =back
    
    =cut
    
    sub _FixNames {
        # Create the result list.
        my @result = ( );
        # Loop through the incoming parameters.
        for my $field (@_) {
            push @result, _FixName($field);
        }
        # Return the result.
        return @result;
    }
    
    =head3 AddField
    
    Add a field to a field list.
    
    This is a static method.
    
    =over 4
    
    =item structure
    
    Structure (usually an entity or relationship) that is to contain the field.
    
    =item fieldName
    
    Name of the new field.
    
    =item fieldData
    
    Structure containing the data to put in the field.
    
    =back
    
    =cut
    
    sub _AddField {
        # Get the parameters.
        my ($structure, $fieldName, $fieldData) = @_;
        # Create the field structure by copying the incoming data.
        my $fieldStructure = {%{$fieldData}};
        # Get a reference to the field list itself.
        my $fieldList = $structure->{Fields};
        # Add the field to the field list.
        $fieldList->{$fieldName} = $fieldStructure;
    }
    
    =head3 ReOrderRelationTable
    
    This method will take a relation table and re-sort it according to the implicit ordering of the
    C<PrettySort> property. Instead of a hash based on field names, it will return a list of fields.
    This requires creating a new hash that contains the field name in the C<name> property but doesn't
    have the C<PrettySort> property, and then inserting that new hash into the field list.
    
    This is a static method.
    
    =over 4
    
    =item relationTable
    
    Relation hash to be reformatted into a list.
    
    =item RETURN
    
    A list of field hashes.
    
    =back
    
    =cut
    
    sub _ReOrderRelationTable {
        # Get the parameters.
        my ($relationTable) = @_;
        # Create the return list.
        my @resultList;
        # Rather than copy all the fields in a single pass, we make multiple passes and only copy
        # fields whose PrettySort value matches the current pass number. This process continues
        # until we process all the fields in the relation.
        my $fieldsLeft = (values %{$relationTable});
        for (my $sortPass = 1; $fieldsLeft > 0; $sortPass++) {
            # Loop through the fields. Note that we lexically sort the fields. This makes field name
            # secondary to pretty-sort number in the final ordering.
            for my $fieldName (sort keys %{$relationTable}) {
                # Get this field's data.
                my $fieldData = $relationTable->{$fieldName};
                # Verify the sort pass.
                if ($fieldData->{PrettySort} == $sortPass) {
                    # Here we're in the correct pass. Denote we've found a field.
                    $fieldsLeft--;
                    # The next step is to create the field structure. This done by copying all
                    # of the field elements except PrettySort and adding the name.
                    my %thisField;
                    for my $property (keys %{$fieldData}) {
                        if ($property ne 'PrettySort') {
                            $thisField{$property} = $fieldData->{$property};
                        }
                    }
                    $thisField{name} = $fieldName;
                    # Now we add this field to the end of the result list.
                    push @resultList, \%thisField;
                }
            }
        }
        # Return a reference to the result list.
        return \@resultList;
    
    }
    
    =head3 IsPrimary
    
    Return TRUE if a specified relation is a primary relation, else FALSE. A relation is primary
    if it has the same name as an entity or relationship.
    
    This is an instance method.
    
    =over 4
    
    =item relationName
    
    Name of the relevant relation.
    
    =item RETURN
    
    Returns TRUE for a primary relation, else FALSE.
    
    =back
    
    =cut
    
    sub _IsPrimary {
        # Get the parameters.
        my ($self, $relationName) = @_;
        # Check for the relation in the entity table.
        my $entityTable = $self->{_metaData}->{Entities};
        my $retVal = exists $entityTable->{$relationName};
        if (! $retVal) {
            # Check for it in the relationship table.
            my $relationshipTable = $self->{_metaData}->{Relationships};
            $retVal = exists $relationshipTable->{$relationName};
        }
        # Return the determination indicator.
        return $retVal;
    }
    
    =head3 FindRelation
    
    Return the descriptor for the specified relation.
    
    This is an instance method.
    
    =over 4
    
    =item relationName
    
    Name of the relation whose descriptor is to be returned.
    
    =item RETURN
    
    Returns the object that describes the relation's indexes and fields.
    
    =back
    
    =cut
    sub _FindRelation {
        # Get the parameters.
        my ($self, $relationName) = @_;
        # Get the relation's structure from the master relation table in the metadata structure.
        my $metaData = $self->{_metaData};
        my $retVal = $metaData->{RelationTable}->{$relationName};
        # Return it to the caller.
        return $retVal;
    }
    
    =head2 HTML Documentation Utility Methods
    
    =head3 ComputeRelationshipSentence
    
    The relationship sentence consists of the relationship name between the names of the
    two related entities and an arity indicator.
    
    This is a static method.
    
    =over 4
    
    =item relationshipName
    
    Name of the relationship.
    
    =item relationshipStructure
    
    Relationship structure containing the relationship's description and properties.
    
    =item RETURN
    
    Returns a string containing the entity names on either side of the relationship name and an
    indicator of the arity.
    
    =back
    
    =cut
    
    sub _ComputeRelationshipSentence {
        # Get the parameters.
        my ($relationshipName, $relationshipStructure) = @_;
        # Format the relationship sentence.
        my $result = "$relationshipStructure->{from} <b>$relationshipName</b> $relationshipStructure->{to}";
        # Compute the arity.
        my $arityCode = $relationshipStructure->{arity};
        my $arity = $ArityTable{$arityCode};
        $result .= " ($arity)";
        return $result;
    }
    
    =head3 ComputeRelationshipHeading
    
    The relationship heading is the L<relationship sentence|/ComputeRelationshipSentence> with the entity
    names hyperlinked to the appropriate entity sections of the document.
    
    This is a static method.
    
    =over 4
    
    =item relationshipName
    
    Name of the relationship.
    
    =item relationshipStructure
    
    Relationship structure containing the relationship's description and properties.
    
    =item RETURN
    
    Returns a string containing the entity names on either side of the relationship name with the entity
    names hyperlinked.
    
    =back
    
    =cut
    
    sub _ComputeRelationshipHeading {
        # Get the parameters.
        my ($relationshipName, $relationshipStructure) = @_;
        # Get the FROM and TO entity names.
        my $fromEntity = $relationshipStructure->{from};
        my $toEntity = $relationshipStructure->{to};
        # Format a relationship sentence with hyperlinks in it.
        my $result = "<a href=\"#$fromEntity\">$fromEntity</a> $relationshipName <a href=\"#$toEntity\">$toEntity</a>";
        return $result;
    }
    
    =head3 ShowRelationTable
    
    Generate the HTML string for a particular relation. The relation's data will be formatted as an HTML
    table with three columns-- the field name, the field type, and the field description.
    
    This is a static method.
    
    =over 4
    
    =item relationName
    
    Name of the relation being formatted.
    
    =item relationData
    
    Hash containing the relation's fields and indexes.
    
    =item RETURN
    
    Returns an HTML string that can be used to display the relation name and all of its fields.
    
    =back
    
    =cut
    
    sub _ShowRelationTable {
        # Get the parameters.
        my ($relationName, $relationData) = @_;
        # Start the relation's field table.
        my $htmlString = _OpenFieldTable($relationName);
        # Loop through the fields.
        for my $field (@{$relationData->{Fields}}) {
            $htmlString .= _ShowField($field);
        }
        # Close this relation's field table.
        $htmlString .= &_CloseTable;
        # Now we show the relation's indexes.
        $htmlString .= "<ul>\n";
        my $indexTable = $relationData->{Indexes};
        for my $indexName (sort keys %{$indexTable}) {
            my $indexData = $indexTable->{$indexName};
            # Determine whether or not the index is unique.
            my $fullName = $indexName;
            if (exists $indexData->{Unique} && $indexData->{Unique} eq "true") {
                $fullName .= " (unique)";
            }
            # Start an HTML list item for this index.
            $htmlString .= "<li><b>Index $fullName</b>\n<ul>\n";
            # Add any note text.
            if (my $note = $indexData->{Notes}) {
                $htmlString .= "<li>" . _HTMLNote($note->{content}) . "</li>\n";
            }
            # Add the fiield list.
            $htmlString .= "<li><i>" . join(', ', @{$indexData->{IndexFields}}) . "</i></li>\n";
            # Close this entry.
            $htmlString .= "</ul></li>\n";
        }
        # Close off the index list.
        $htmlString .= "</ul>\n";
    }
    
    =head3 OpenFieldTable
    
    This method creates the header string for the field table generated by L</ShowMetaData>.
    
    This is a static method.
    
    =over 4
    
    =item tablename
    
    Name of the table whose fields will be displayed.
    
    =item RETURN
    
    Returns a string containing the HTML for a field table's header.
    
    =back
    
    =cut
    
    sub _OpenFieldTable {
        my ($tablename) = @_;
        return _OpenTable($tablename, 'Field', 'Type', 'Description');
    }
    
    =head3 OpenTable
    
    This method creates the header string for an HTML table.
    
    This is a static method.
    
    =over 4
    
    =item tablename
    
    Title of the table.
    
    =item colName1, colName2, ..., colNameN
    
    List of column names.
    
    =item RETURN
    
    Returns a string containing the HTML for the desired table's header.
    
    =back
    
    =cut
    
    sub _OpenTable {
        # Get the parameters.
        my ($tablename, @colNames) = @_;
        # Compute the number of columns.
        my $colCount = @colNames;
        # Generate the title row.
        my $htmlString = "<p><table border=\"2\"><tr><td colspan=\"$colCount\" align=\"center\">$tablename</td></tr>\n";
        # Loop through the columns, adding the column header rows.
        $htmlString .= "<tr>";
        for my $colName (@colNames) {
            $htmlString .= "<th>$colName</th>";
        }
        $htmlString .= "</tr>\n";
        return $htmlString;
    }
    
    =head3 CloseTable
    
    This method returns the HTML for closing a table.
    
    This is a static method.
    
    =cut
    
    sub _CloseTable {
        return "</table></p>\n";
    }
    
    =head3 ShowField
    
    This method returns the HTML for displaying a row of field information in a field table.
    
    This is a static method.
    
    =over 4
    
    =item fieldData
    
    Table of data about the field.
    
    =item RETURN
    
    Returns an HTML string for a table row that shows the field's name, type, and description.
    
    =back
    
    =cut
    
    sub _ShowField {
        # Get the parameters.
        my ($fieldData) = @_;
        # Create the HTML string.
        my $htmlString = "<tr><th align=\"left\">$fieldData->{name}</th><td>$fieldData->{type}</td>";
        # If we have content, add it as a third column.
        if (exists $fieldData->{Notes}) {
            $htmlString .= "<td>" . _HTMLNote($fieldData->{Notes}->{content}) . "</td>";
        }
        # Close off the row.
        $htmlString .= "</tr>\n";
        # Return the result.
        return $htmlString;
    }
    
    =head3 HTMLNote
    
    Convert a note or comment to HTML by replacing some bulletin-board codes with HTML. The codes
    supported are C<[b]> for B<bold>, C<[i]> for I<italics>, and C<[p]> for a new paragraph.
    Except for C<[p]>, all the codes are closed by slash-codes. So, for
    example, C<[b]Feature[/b]> displays the string C<Feature> in boldface.
    
    This is a static method.
    
    =over 4
    
    =item dataString
    
    String to convert to HTML.
    
    =item RETURN
    
    An HTML string derived from the input string.
    
    =back
    
    =cut
    
    sub _HTMLNote {
        # Get the parameter.
        my ($dataString) = @_;
        # Substitute the codes.
        $dataString =~ s!\[(/?[bi])\]!<$1>!g;
        $dataString =~ s!\[p\]!</p><p>!g;
        # Return the result.
        return $dataString;
    }
    
    =head2 Data Generation Utilities
    
    =head3 IntGen
    
    C<< my $integer = IntGen($min, $max); >>
    
    Returns a random number between the specified minimum and maximum (inclusive).
    
    =over 4
    
    =item min
    
    Minimum permissible return value.
    
    =item max
    
    Maximum permissible return value.
    
    =item RETURN
    
    Returns a value no lower than the minimum and no greater than the maximum.
    
    =back
    
    =cut
    
    sub IntGen {
        # Get the parameters.
        my ($min, $max) = @_;
        # Determine the range of possible values. Note we put some space well above the
        # maximum value to give it a fighting chance of apppearing in the list.
        my $span = $max + 0.99 - $min;
        # Create an integer in the range.
        my $retVal = $min + int(rand($span));
        # Return the result.
        return $retVal;
    }
    
    =head3 RandChar
    
    C<< my $char = RandChar($sourceString); >>
    
    Select a random character from a string.
    
    =over 4
    
    =item sourceString
    
    String from which the random character should be selected.
    
    =item RETURN
    
    Returns a single character from the incoming string.
    
    =back
    
    =cut
    
    sub RandChar {
        # Get the parameter.
        my ($sourceString) = @_;
        # Select a random character.
        my $retVal = IntGen(0, (length $sourceString) - 1);
        # Return it.
        return substr($sourceString, $retVal, 1);
    }
    
    =head3 RandChars
    
    C<< my $string = RandChars($sourceString, $length); >>
    
    Create a string from characters taken from a source string.
    
    =over 4
    
    =item sourceString
    
    String from which the random characters should be selected.
    
    =item length
    
    Number of characters to put in the output string.
    
    =item RETURN
    
    Returns a string of the specified length consisting of characters taken from the
    source string.
    
    =back
    
    =cut
    
    sub RandChars {
        # Get the parameters.
        my ($sourceString, $length) = @_;
        # Call RandChar repeatedly to generate the string.
        my $retVal = "";
        for (my $i = 0; $i < $length; $i++) {
            $retVal .= RandChar($sourceString);
        }
        # Return the result.
        return $retVal;
    }
    
    =head3 RandParam
    
    C<< my $value = RandParam($parm1, $parm2, ... $parmN); >>
    
    Return a randomly-selected value from the parameter list.
    
    =over 4
    
    =item parm1, parm2, ... parmN
    
    List of values of which one will be selected.
    
    =item RETURN
    
    Returns a randomly-chosen value from the specified list.
    
    =back
    
    =cut
    
    sub RandParam {
        # Get the parameter.
        my @parms = @_;
        # Choose a random parameter from the list.
        my $chosenIndex = IntGen(0, $#parms);
        return $parms[$chosenIndex];
    }
    
    =head3 StringGen
    
    C<< my $string = StringGen($pattern1, $pattern2, ... $patternN); >>
    
    Returns a random string derived from a randomly-chosen format pattern. The pattern
    can either be a number (indicating the number of characters desired, or the letter
    C<P> followed by a picture. The picture should contain C<A> when a letter is desired,
    C<9> when a digit is desired, C<V> when a vowel is desired, C<K> when a consonant is
    desired, and C<X> when a letter or a digit is desired. Any other character will be
    translated as a literal.
    
    =over 4
    
    =item pattern1, pattern2, ... patternN
    
    List of patterns to be used to generate string values.
    
    =item RETURN
    
    A single string generated from a pattern.
    
    =back
    
    =cut
    
    sub StringGen {
        # Get the parameters.
        my @patterns = @_;
        # Choose the appropriate pattern.
        my $chosenPattern = RandParam(@patterns);
        # Declare the return variable.
        my $retVal = "";
        # Determine whether this is a count or a picture pattern.
        if ($chosenPattern =~ m/^\d+/) {
            # Here we have a count. Get the string of source characters.
            my $letterString = $PictureTable{'X'};
            my $stringLen = length $letterString;
            # Save the number of characters we have to generate.
            my $charsLeft = $chosenPattern;
            # Loop until the return variable is full.
            while ($charsLeft > 0) {
                # Generate a random position in the soruce string.
                my $stringIndex = IntGen(0, $stringLen - 1);
                # Compute the number of characters to pull out of the source string.
                my $chunkSize = $stringLen - $stringIndex;
                if ($chunkSize > $charsLeft) { $chunkSize = $charsLeft; }
                # Stuff this chunk into the return value.
                $retVal .= substr($letterString, $stringIndex, $chunkSize);
                # Record the data moved.
                $charsLeft -= $chunkSize;
            }
        } elsif ($chosenPattern =~ m/^P/) {
            # Here we have a picture string. We will move through the picture one
            # character at a time generating data.
            for (my $i = 1; $i < length $chosenPattern; $i++) {
                # Get this picture character.
                my $chr = substr($chosenPattern, $i, 1);
                # Check to see if the picture char is one we recognize.
                if (exists $PictureTable{$chr}) {
                    # Choose a random character from the available values for this
                    # picture character.
                    $retVal .= RandChar($PictureTable{$chr});
                } else {
                    # Copy in the picture character as a literal.
                    $retVal .= $chr;
                }
            }
        } else {
            # Here we have neither a picture string or a letter count, so we treat
            # the string as a literal.
            $retVal = $chosenPattern;
        }
        # Return the string formed.
        return $retVal;
    }
    
    =head3 DateGen
    
    C<< my $date = DateGen($startDayOffset, $endDayOffset, $minutes); >>
    
    Return a numeric timestamp within the specified range of days with the specified minute
    value. The range of days is specified relevant to the current day. Thus, the call
    
    C<< my $date = DateGen(-1, 5, 720); >>
    
    will return a timestamp at noon (72 minutes past midnight) sometime during the week that
    began on the preceding day. If you want a random minute of the day, simply combine with
    a call to L</IntGen>, as follows.
    
    C<< my $date = DateGen(-1, 5, IntGen(0, 1439)); >>
    
    =over 4
    
    =item startDayOffset
    
    The earliest day that can be returned, relative to the current day.
    
    =item endDayOffset
    
    The latest day that can be returned, related to the current day.
    
    =item minutes
    
    Number of minutes into the selected day that should be used.
    
    =back
    
    =cut
    
    sub DateGen {
        # Get the parameters.
        my ($startDayOffset, $endDayOffset, $minutes) = @_;
        # Get midnight of the current day.
        my $now = time();
        my ($sec, $min, $hour) = localtime($now);
        my $today = $now - (($hour * 60 + $min) * 60 + $sec);
        # Compute the day we want.
        my $newDay = IntGen($startDayOffset, $endDayOffset) * 86400 + $today;
        # Add the minutes.
        my $retVal = $newDay + $minutes * 60;
        # Return the result.
        return $retVal;
    }
    
    =head3 FloatGen
    
    C<< my $number = FloatGen($min, $max); >>
    
    Return a random floating-point number greater than or equal to the specified minimum and
    less than the specified maximum.
    
    =over 4
    
    =item min
    
    Minimum permissible value for the number returned.
    
    =item max
    
    Maximum permissible value for the number returned.
    
    =item RETURN
    
    Returns a floating-point number anywhere in the specified range.
    
    =back
    
    =cut
    
    sub FloatGen {
        # Get the parameters.
        my ($min, $max) = @_;
        # Generate the result.
        my $retVal = rand($max - $min) + $min;
        return $retVal;
    }
    
    =head3 ListGen
    
    C<< my @list = ListGen($pattern, $count); >>
    
    Return a list containing a fixed number of randomly-generated strings.
    
    =over 4
    
    =item pattern
    
    A pattern (in the form expected by L</StringGen>) that should be used to generate the
    strings in the list.
    
    =item count
    
    The number of list entries to generate.
    
    =item RETURN
    
    Returns a list consisting of the specified number of strings.
    
    =back
    
    =cut
    
    sub ListGen {
        # Get the parameters.
        my ($pattern, $count) = @_;
        # Generate the list.
        my @retVal = ();
        for (my $i = 0; $i < $count; $i++) {
            push @retVal, StringGen($pattern);
        }
        # Return it.
        return @retVal;
    }
    
    1;