text:
Parent Directory | Revision Log
Change to make the TO index unique for one-to-many relationships.
package ERDB; use strict; 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. =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 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 =back =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
, 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. 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, $class; 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 "\n\n<title>$title</title>\n"; print HTMLOUT "\n\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 "
\n
Entities\n
\n"; # Loop through the Entities, displaying a list item for each. foreach my $key (sort keys %{$entityList}) { # Display this item. print HTMLOUT "
$key
\n"; } # Close off the entity section and start the relationship section. print HTMLOUT "

\n
Relationships\n
\n"; # Loop through the Relationships. foreach my $key (sort keys %{$relationshipList}) { # Display this item. my $relationshipTitle = _ComputeRelationshipSentence($key, $relationshipList->{$key}); print HTMLOUT "
$relationshipTitle
\n"; } # Close off the relationship section and list the join table section. print HTMLOUT "

\n
Join Table
\n"; # Close off the table of contents itself. print HTMLOUT "
\n"; # Now we start with the actual data. Denote we're starting the entity section. print HTMLOUT "
Entities
\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 "
$key
\n"; # Get the entity data. my $entityData = $entityList->{$key}; # If there's descriptive text, display it. if (my $notes = $entityData->{Notes}) { print HTMLOUT "
" . _HTMLNote($notes->{content}) . "
\n"; } # Now we want a list of the entity's relationships. First, we set up the relationship subsection. print HTMLOUT "
Relationships for $key
\n
\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 "
$relationshipDescription
\n"; } } # Close off the relationship list. print HTMLOUT "
\n"; # Get the entity's relations. my $relationList = $entityData->{Relations}; # Create a header for the relation subsection. print HTMLOUT "
Relations for $key
\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 "
Relationships
\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 "
$headerText
\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 "
Each $fromEntity relates to at most one $toEntity.\n"; } else { print HTMLOUT "
Each $fromEntity relates to multiple $toEntitys.\n"; if ($arity eq "MM" && $fromEntity ne $toEntity) { print HTMLOUT "Each $toEntity relates to multiple $fromEntitys.\n"; } } print HTMLOUT "
\n"; # If there are notes on this relationship, display them. if (my $notes = $relationshipStructure->{Notes}) { print HTMLOUT "
" . _HTMLNote($notes->{content}) . "
\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 "
Join Table
\n"; # Create a table header. print HTMLOUT _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(4); my $source = $self->ComputeObjectSentence($sourceRelation); my $target = $self->ComputeObjectSentence($targetRelation); my $clause = $joinTable->{$joinKey}; # Display them in a table row. print HTMLOUT "
$source	$target	$clause
\n"; } # Close the table. print HTMLOUT _CloseTable(); # Close the document. print HTMLOUT "\n\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. my $entityHash = $metadata->{Entities}; for my $entityName (keys %{$entityHash}) { my $entityData = $entityHash->{$entityName}; # 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 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. $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 GetList C<< my @dbObjects = $database->GetList(\@objectNames, $filterClause, $param1, $param2, ..., $paramN); >> 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 param1, param2, ..., paramN 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 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}; 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 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. 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 = $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(2); # 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(2); # Re-create the table without its index. $self->CreateTable($relationName, 0); # If this is a pre-index DBMS, create the index here. if ($dbh->{_preIndex}) { eval { $self->CreateIndex($relationName); }; if ($@) { $retVal->AddMessage($@); } } } # 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; # 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; my $inputCount = 0; # Loop through the file. while (<TABLEIN>) { $inputCount++; # Chop off the new-line character. my $record = Tracer::Strip($_); # Only proceed if the record is non-blank. if ($record) { # Escape all the backslashes found in the line. $record =~ s/\\/\\\\/g; # Insure the number of fields is correct. my @fields = split /\t/, $record; while (@fields > $fieldCount) { my $extraField = $fields[$#fields]; delete $fields[$#fields]; if ($extraField) { Trace("Nonblank extra field value \"$extraField\" deleted from record $inputCount of $fileName.") if T(1); } } while (@fields < $fieldCount) { push @fields, ""; } # 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) { push @fields, "0"; } # Write the record. $record = join "\t", @fields; print TABLEOUT "$record\n"; # Count the record written. my $count = $retVal->Add('records'); my $len = length $record; Trace("Record $count written with $len characters.") if T(4); } else { # Here we have a blank record. $retVal->Add('skipped'); } } # Close the files. close TABLEIN; close TABLEOUT; Trace("Temporary file $tempName created.") if T(2); # 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(2); # If we're rebuilding, we need to create the table indexes. if ($truncateFlag && ! $dbh->{_preIndex}) { 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; } =head3 GetEntity C<< my $entityObject = $sprout->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 = 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 = $sprout->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 = $sprout->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") { @parmList = @{$parameterList}; } else { push @parmList, $parameterList; } # Create the query. my $query = $self->Get($objectNames, $filterClause, @parmList); # Set up a counter of the number of records read. my $fetched = 0; # Insure the counter has a value. if (!defined $count) { $count = 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++; } # Return the resulting list. return @retVal; } =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}; 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 = "id of the source $fromEntity."; # 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 = "id of the target $toEntity."; # 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(3); 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(3); 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(3); } else { # Here the relationship comes from the entity. push @fromList, $relationshipName; Trace("Relationship $relationshipName put in from-list.") if T(3); } } elsif ($toEntity eq $entityName) { # Here the relationship goes to the entity. push @toList, $relationshipName; Trace("Relationship $relationshipName put in to-list.") if T(3); } } # 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(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(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(3); # 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(3); # 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; # 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} $relationshipName $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 = "$fromEntity $relationshipName $toEntity"; 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 .= "
\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 .= "
Index $fullName\n
\n"; # Add any note text. if (my $note = $indexData->{Notes}) { $htmlString .= "
" . _HTMLNote($note->{content}) . "
\n"; } # Add the fiield list. $htmlString .= "
" . join(', ', @{$indexData->{IndexFields}}) . "
\n"; # Close this entry. $htmlString .= "

\n"; } # Close off the index list. $htmlString .= "
\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 = "

$tablename
\n"; # Loop through the columns, adding the column header rows. $htmlString .= "
"; for my $colName (@colNames) { $htmlString .= "$colName	"; } $htmlString .= "
\n"; return $htmlString; } =head3 CloseTable This method returns the HTML for closing a table. This is a static method. =cut sub _CloseTable { return "

\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 = "
$fieldData->{name}	$fieldData->{type}	"; # If we have content, add it as a third column. if (exists $fieldData->{Notes}) { $htmlString .= "" . _HTMLNote($fieldData->{Notes}->{content}) . "	"; } # Close off the row. $htmlString .= "
\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\]!

!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
 followed by a picture. The picture should contain C 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