[Bio] / Sprout / ERDB.pm Repository:
ViewVC logotype

View of /Sprout/ERDB.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.5 - (download) (as text) (annotate)
Tue Apr 5 05:17:01 2005 UTC (14 years, 7 months ago) by parrello
Branch: MAIN
Changes since 1.4: +10 -5 lines
*** empty log message ***

package ERDB;

	use strict;
	use Carp;
	use Tracer;
	use DBKernel;
	use Data::Dumper;
	use XML::Simple;
	use DBQuery;
	use DBObject;
	use Stats;
	use Time::HiRes qw(gettimeofday);

=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.

=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.
my %TypeTable = ( char =>	 { sqlType => 'CHAR(1)',			maxLen => 1,			dataGen => "StringGen('A')" },
				  int =>	 { sqlType => 'INTEGER',			maxLen => 20,			dataGen => "IntGen(0, 99999999)" },
				  string =>  { sqlType => 'VARCHAR(255)',		maxLen => 255,			dataGen => "StringGen(IntGen(10,250))" },
				  text =>	 { sqlType => 'TEXT',				maxLen => 1000000000,	dataGen => "StringGen(IntGen(80,1000))" },
				  date =>	 { sqlType => 'BIGINT',				maxLen => 80,			dataGen => "DateGen(-7, 7, IntGen(0,1400))" },
				  float =>	 { sqlType => 'DOUBLE PRECISION',	maxLen => 40,			dataGen => "FloatGen(0.0, 100.0)" },
				  boolean => { sqlType => 'SMALLINT',			maxLen => 1,			dataGen => "IntGen(0, 1)" },
			     'key-string' =>
							 { sqlType => 'VARCHAR(40)',		maxLen => 40,			dataGen => "StringGen(IntGen(10,40))" },
				 'name-string' =>
							 { sqlType => 'VARCHAR(80)',		maxLen => 80,			dataGen => "StringGen(IntGen(10,80))" },
				 'medium-string' =>
							 { sqlType => 'VARCHAR(160)',		maxLen => 160,			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;
	return $self;
}

=head3 ShowMetaData

C<< $database->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";
	# 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.
	print HTMLOUT "<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.
		print HTMLOUT "<li><a href=\"#$key\">$key</a></li>\n";
	}
	# Close off the entity section and start the relationship section.
	print HTMLOUT "</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});
		print HTMLOUT "<li><a href=\"#$key\">$relationshipTitle</a></li>\n";
	}
	# Close off the relationship section and list the join table section.
	print HTMLOUT "</ol></li>\n<li><a href=\"#JoinTable\">Join Table</a></li>\n";
	# Close off the table of contents itself.
	print HTMLOUT "</ul>\n";
	# Now we start with the actual data. Denote we're starting the entity section.
	print HTMLOUT "<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.
		print HTMLOUT "<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}) {
			print HTMLOUT "<p>" . _HTMLNote($notes->{content}) . "</p>\n";
		}
		# Now we want a list of the entity's relationships. First, we set up the relationship subsection.
		print HTMLOUT "<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.
				print HTMLOUT "<li><a href=\"#$relationship\">$relationshipDescription</a></li>\n";
			}
		}
		# Close off the relationship list.
		print HTMLOUT "</ul>\n";
		# Get the entity's relations.
		my $relationList = $entityData->{Relations};
		# Create a header for the relation subsection.
		print HTMLOUT "<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});
			print HTMLOUT $htmlString;
		}
	}
	# Denote we're starting the relationship section.
	print HTMLOUT "<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);
		print HTMLOUT "<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") {
			print HTMLOUT "<p>Each <b>$fromEntity</b> relates to at most one <b>$toEntity</b>.\n";
		} else {
			print HTMLOUT "<p>Each <b>$fromEntity</b> relates to multiple <b>$toEntity</b>s.\n";
			if ($arity eq "MM" && $fromEntity ne $toEntity) {
				print HTMLOUT "Each <b>$toEntity</b> relates to multiple <b>$fromEntity</b>s.\n";
			}
		}
		print HTMLOUT "</p>\n";
		# If there are notes on this relationship, display them.
		if (my $notes = $relationshipStructure->{Notes}) {
			print HTMLOUT "<p>" . _HTMLNote($notes->{content}) . "</p>\n";
		}
		# Generate the relationship's relation table.
		my $htmlString = _ShowRelationTable($key, $relationshipStructure->{Relations}->{$key});
		print HTMLOUT $htmlString;
	}
	Trace("Building MetaData join table.") if T(4);
	# Denote we're starting the join table.
	print HTMLOUT "<a name=\"JoinTable\"></a><h3>Join Table</h3>\n";
	# Create a table header.
	print HTMLOUT _OpenTable("Join Table", "Source", "Target", "Join Condition");
	# Loop through the joins.
	my $joinTable = $metadata->{Joins};
	for my $joinKey (sort keys %{$joinTable}) {
		# Separate out the source, the target, and the join clause.
		$joinKey =~ m!([^/]*)/(.*)$!;
		my ($source, $target, $clause) = ($self->ComputeObjectSentence($1),
										  $self->ComputeObjectSentence($2),
										  $joinTable->{$joinKey});
		# Display them in a table row.
		print HTMLOUT "<tr><td>$source</td><td>$target</td><td>$clause</td></tr>\n";
	}
	# Close the table.
	print HTMLOUT _CloseTable();
	# Close the document.
	print HTMLOUT "</body>\n</html>\n";
	# Close the file.
	close HTMLOUT;
	Trace("Built MetaData web page.") if T(3);
}

=head3 DumpMetaData

C<< $database->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<< $datanase->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) = @_;
	my $metadata = $self->{_metaData};
	my $dbh = $self->{_dbh};
	# Loop through the entities.
	while (my ($entityName, $entityData) = each %{$metadata->{Entities}}) {
		# Tell the user what we're doing.
		Trace("Creating relations for entity $entityName.") if T(1);
		# Loop through the entity's relations.
		for my $relationName (keys %{$entityData->{Relations}}) {
			# Create a table for this relation.
			$self->CreateTable($relationName);
			Trace("Relation $relationName created.") if T(1);
		}
	}
	# Loop through the relationships.
	my $relationshipTable = $metadata->{Relationships};
	for my $relationshipName (keys %{$metadata->{Relationships}}) {
		# Create a table for this relationship.
		Trace("Creating relationship $relationshipName.") if T(1);
		$self->CreateTable($relationshipName);
	}
}

=head3 CreateTable

C<< $database->CreateTable($tableName, $indexFlag); >>

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.

=back

=cut

sub CreateTable {
	# Get the parameters.
	my ($self, $relationName, $indexFlag) = @_;
	# 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);
	# Create the table.
	Trace("Creating table $relationName: $fieldThing") if T(2);
	$dbh->create_table(tbl => $relationName, flds => $fieldThing);
	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 CreateIndex

C<< $database->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 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.
	while (my ($indexName, $indexData) = each %{$relationData->{Indexes}}) {
		# 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.
		$dbh->create_index(idx => $indexName, tbl => $relationName, flds => $flds, unique => $unique);
		Trace("Index created: $indexName for $relationName ($flds)") if T(1);
	}
}

=head3 LoadTables

C<< my $stats = $database->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 metadata structure.
	my $metaData = $self->{_metaData};
	# Loop through the entities.
	for my $entity (values %{$metaData->{Entities}}) {
		# Loop through the entity's relations.
		for my $relationName (keys %{$entity->{Relations}}) {
			# Try to load this relation.
			my $result = $self->_LoadRelation($directoryName, $relationName, $rebuild);
			# Accumulate the statistics.
			$retVal->Accumulate($result);
		}
	}
	# Loop through the relationships.
	for my $relationshipName (keys %{$metaData->{Relationships}}) {
		# Try to load this relationship's relation.
		my $result = $self->_LoadRelation($directoryName, $relationshipName, $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 = $database->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 = $database->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 Get

C<< my $query = $database->Get(\@objectNames, $filterClause, $param1, $param2, ..., $paramN); >>

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 = $sprout->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 = $sprout->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 = $sprout->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, you can't specify any entity or relationship more than once, and 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.

=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 param1, param2, ..., paramN

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) = @_;
	# Construct the SELECT statement. The general pattern is
	#
	# SELECT name1.*, name2.*, ... nameN.* FROM name1, name2, ... nameN
	#
	my $dbh = $self->{_dbh};
	my $command = "SELECT DISTINCT " . join('.*, ', @{$objectNames}) . ".* FROM " .
				join(', ', @{$objectNames});
	# 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) } @{$objectNames};
		# 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.
		my %fromNames = ();
		for my $objectName (@sortedNames) {
			$fromNames{$objectName} = 1;
		}
		# 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 $objectName (@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 $objectName;
			# Get the object's field list.
			my $fieldList = $self->_GetFieldTable($objectName);
			# Find the field references for this object.
			while ($filterString =~ m/$objectName\(([^)]*)\)/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 {
					# Get the field's relation.
					my $relationName = $fieldList->{$fieldName}->{relation};
					# Insure the relation is in the FROM clause.
					if (!exists $fromNames{$relationName}) {
						# Add the relation to the FROM clause.
						$command .= ", $relationName";
						# Create its join sub-clause.
						push @joinWhere, "$objectName.id = $relationName.id";
						# Denote we have it available for future fields.
						$fromNames{$relationName} = 1;
					}
					# Form an SQL field reference from the relation name and the field name.
					my $sqlReference = "$relationName." . _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 @objectList = @{$objectNames};
		my $lastObject = shift @objectList;
		# Get the join table.
		my $joinTable = $self->{_metaData}->{Joins};
		# Loop through the object list.
		for my $thisObject (@objectList) {
			# Look for a join.
			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 $lastObject to $thisObject.");
			} else {
				# Get the join clause and add it to the WHERE list.
				push @joinWhere, $joinTable->{$joinKey};
				# Save this object as the last object for the next iteration.
				$lastObject = $thisObject;
			}
		}
		# Now we need to handle the whole ORDER BY thing. We'll put the order by clause
		# in the following variable.
		my $orderClause = "";
		# Locate the ORDER BY verb (if any).
		if ($filterString =~ m/^(.*)ORDER BY/g) {
			# Here we have an ORDER BY verb. Split it off of the filter string.
			my $pos = pos $filterString;
			$orderClause = substr($filterString, $pos);
			$filterString = $1;
		}
		# Add the filter and the join clauses (if any) to the SELECT command.
		if ($filterString) {
			push @joinWhere, "($filterString)";
		}
		if (@joinWhere) {
			$command .= " WHERE " . join(' AND ', @joinWhere);
		}
		# Add the sort clause (if any) to the SELECT command.
		if ($orderClause) {
			$command .= " ORDER BY $orderClause";
		}
	}
	Trace("SQL query: $command") if T(2);
	Trace("PARMS: '" . (join "', '", @params) . "'") if (T(3) && (@params > 0));
	my $sth = $dbh->prepare_command($command);
	# Execute it with the parameters bound in.
	$sth->execute(@params) || Confess("SELECT error" . $sth->errstr());
	# Return the statement object.
	my $retVal = DBQuery::_new($self, $sth, @{$objectNames});
	return $retVal;
}

=head3 ComputeObjectSentence

C<< my $sentence = $database->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<< $database->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};
	while (my ($entityName, $entityStructure) = each %{$entities}) {
		# Get the entity's relations.
		my $relationList = $entityStructure->{Relations};
		# Loop through the relations, dumping them.
		while (my ($relationName, $relation) = each %{$relationList}) {
			$self->_DumpRelation($outputDirectory, $relationName, $relation);
		}
	}
	# Next, we loop through the relationships.
	my $relationships = $metaData->{Relationships};
	while (my ($relationshipName, $relationshipStructure) = each %{$relationships}) {
		# Dump this relationship's relation.
		$self->_DumpRelation($outputDirectory, $relationshipName, $relationshipStructure->{Relations}->{$relationshipName});
	}
}

=head3 InsertObject

C<< my $ok = $database->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<< $database->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<< $database->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.
	while ($retVal && (my ($relationName, $relationDefinition) = each %{$relationTable})) {
		# 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 = $database->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 the number of records read and 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(1);
	# Get the database handle.
	my $dbh = $self->{_dbh};
	# Get the relation data.
	my $relation = $self->_FindRelation($relationName);
	# Check the truncation flag.
	if ($truncateFlag) {
		Trace("Creating table $relationName") if T(1);
		# Re-create the table without its index.
		$self->CreateTable($relationName, 0);
	}
	# Determine whether or not this is a primary relation. Primary relations have an extra
	# field indicating whether or not a given object is new or was loaded from the flat files.
	my $primary = $self->_IsPrimary($relationName);
	# Get the number of fields in this relation.
	my @fieldList = @{$relation->{Fields}};
	my $fieldCount = @fieldList;
	# Record the number of expected fields.
	my $expectedFields = $fieldCount + ($primary ? 1 : 0);
	# Start a database transaction.
	$dbh->begin_tran;
	# Open the relation file. We need to create a cleaned-up copy before loading.
	open TABLEIN, '<', $fileName;
	my $tempName = "$fileName.tbl";
	open TABLEOUT, '>', $tempName;
	# Loop through the file.
	while (<TABLEIN>) {
		# Chop off the new-line character.
		my $record = $_;
		chomp $record;
        # Only proceed if the record is non-blank.
        if ($record) {
            # Escape all the backslashes found in the line.
            $record =~ s/\\/\\\\/g;
            # Eliminate any trailing tabs.
            chop $record while substr($record, -1) eq "\t";
            # If this is a primary relation, add a 0 for the new-record flag (indicating that
            # this record is not new, but part of the original load).
            if ($primary) {
                $record .= "\t0";
            }
            # Write the record.
            print TABLEOUT "$record\n";
            # Count the record read.
            my $count = $retVal->Add('records');
            my $len = length $record;
            Trace("Record $count written with $len characters.") if T(4);
        }
	}
	# Close the files.
	close TABLEIN;
	close TABLEOUT;
    Trace("Temporary file $tempName created.") if T(4);
    # Load the table.
	my $rv;
	eval {
		$rv = $dbh->load_table(file => $tempName, tbl => $relationName);
	};
	if (!defined $rv) {
        $retVal->AddMessage($@) if ($@);
        $retVal->AddMessage("Table load failed for $relationName using $tempName.");
		Trace("Table load failed for $relationName.") if T(1);
	} else {
		# Here we successfully loaded the table. Trace the number of records loaded.
		Trace("$retVal->{records} records read for $relationName.") if T(1);
		# If we're rebuilding, we need to create the table indexes.
		if ($truncateFlag) {
			eval {
				$self->CreateIndex($relationName);
			};
			if ($@) {
				$retVal->AddMessage($@);
			}
		}
	}
	# Commit the database changes.
	$dbh->commit_tran;
	# Delete the temporary file.
	unlink $tempName;
	# Return the statistics.
	return $retVal;
}

=head3 GenerateEntity

C<< my $fieldHash = $database->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;
}


=head2 Internal Utility Methods

=head3 GetLoadStats

Return a blank statistics object for use by the load methods.

This is a static method.

=cut

sub _GetLoadStats {
	return Stats->new('records');
}

=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 GetFieldTable

Get the field 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 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};
}

=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) = @_;
	# 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};
	while (my ($entityName, $entityStructure) = each %{$entityList}) {
		#
		# The first step is to run creating 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.
		while (my ($relationName, $relation) = each %{$relationTable}) {
			# 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.
		while (my ($relationName, $relation) = each %{$relationTable}) {
			# 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};
	while (my ($relationshipName, $relationshipStructure) = each %{$relationshipList}) {
		# 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 = ();
		while (my ($relationshipName, $relationship) = each %{$relationshipList}) {
			# Determine if this relationship has our entity in one of its link fields.
			if ($relationship->{from} eq $entityName) {
				if ($relationship->{to} eq $entityName) {
					# Here the relationship is recursive.
					push @bothList, $relationshipName;
				} else {
					# Here the relationship comes from the entity.
					push @fromList, $relationshipName;
				}
			} elsif ($relationship->{to} eq $entityName) {
				# Here the relationship goes to the entity.
				push @toList, $relationshipName;
			}
		}
		# 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 );
		while (my ($linkType, $relationships) = each %directRelationships) {
			# 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";
				$joinTable{"$entityName/$relationshipName"} = $joinClause;
				$joinTable{"$relationshipName/$entityName"} = $joinClause;
				# Create joins between this relationship and the other relationships.
				while (my ($otherType, $otherships) = each %otherRelationships) {
					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};
						} elsif ($otherName ne $relationshipName) {
							# Here we have a valid join. Note that joins between a
							# relationship and itself are prohibited.
							$joinTable{$joinKey} = "$otherName.${otherType}_link = $linkField";
						}
					}
				}
				# 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) {
					# 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) {
			# 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 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;
	# 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.
		while (my ($fieldName, $fieldData) = each %{$structure->{Fields}}) {
			# 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;

MCS Webmaster
ViewVC Help
Powered by ViewVC 1.0.3