package SetDB::Set; use strict; use Debug; use Data::Dumper; use SetDB::Tuple; $Debug::debug{'findpath'} = 0; =head1 NAME SetDB::Set - Holds a set of things (tuples or other sets) =head1 METHODS =over 4 =item new Set($setdb, @sets...) Creates a new set using nested array notation. This is usually called from the main SetDB instance. =cut sub new { my ($class, $setdb, @sets) = @_; my $self = {}; bless $self, $class; $self->{setdb} = $setdb; # And just a shortcut for DBI $self->{dbi} = $setdb->{dbi}; $self->{set} = $self->buildSet(@sets); # Sets in SetBuilder notation $self->{schema} = $setdb->{schema}; # Actual graph of known sets return $self; } =item new buildSet(@sets...) Takes the setBuilder notation and turns it into our local graph called 'set'. =cut sub buildSet { my $self = shift; my @sets = @_; my $graph = new SetDB::Graph; my $primary = shift @sets; sub getPath { my $self = shift; my $table = shift; my @parentSets = @_; # Look up the current table in the graph my $table2 = $self->{graph}->findNode($table); foreach my $t (reverse @parentSets) { # Look up this parent in the graph my $table1 = $self->{graph}->findNode($t); # Now look for a path between these tables if(my $path = $self->{graph}->findPath($table1, $table2)) { # We found one, so we'll just add this link to the path and exit return @{ $path }; } } return undef; } =item $set->getFieldList() This gives the current field list for this set. =cut sub getFieldList { my $self = shift; my $sets = $self->{sets}; # List of fields to be queried my @fields; my @sets = @{$sets}; while(my $table = shift @sets) { if (ref $table) { # Okay, this is a complex childset. Lets give each element in the # childset which has a name an entry in our tuple. If it is a # second-level childset then we will ignore it (They'll have to grab one # of the named ones and ask for the childsets of that). # Create a new set based on the current one my $copy = { %$self }; $copy->{sets} = $table; # This child's Sets consists of just this subset bless $copy, ref $self; # Delete the query and sth so it gets restart $copy->restart(); foreach my $t (@{$table}) { unless (ref $t) { $self->{childset}{$t} = $copy; } } } else { # get a list of the fields for this table my @gr = keys %{$self->{setdb}->{schema}{$table}{fields}}; # add in the refs fields unshift @gr, keys %{$self->{setdb}->{schema}{$table}{refs}}; # This is an extra so we can do updating unshift @gr, "id as ".$table."__id"; unshift @gr, "id"; # add the table name to all these fields @gr = map { $table . '.' . $_ } @gr; push @fields, @gr; } } $self->{fields} = \@fields; my $fields = join(', ',@fields); return $fields; } =item $set->getJoinList() Given a setbuilder-list this recursively builds a SQL join list. We give it the setBuilder list so that we can work recursively =cut sub getJoinListHelper { my $self = shift; # Lets deref the setbuilder-list my @sets = @{ shift(@_) }; my @parentSets = @_; my @joinList; # Ref to list of [table, name] sets. # Okay, lets go through all the tables which we were initially given so that # we can explore relationships and all that jazz while(my $table = shift @sets) { if (ref $table) # This is a sub-set { # Get the subset's join list and add it to ours my @childJoinList = $self->getJoinListHelper($table, @parentSets); push @joinList, @childJoinList; next; } # Is this the first table? if($#parentSets < 0) { # This is the first table so lets just add it to our list. push @joinList, [$table, undef]; push @parentSets, $table; } else { # Lets go through the tables that are already in there and find a path to # the existing table, including an intermediate tables my @path = $self->getPath($table, @parentSets); if(@path) { # We found a path so we just add all the new info to the joinList # Shift off the parent-set node shift @path; while(@path) { my $edge = shift @path; my $node = shift @path; # Add this node/edge to the path list push @joinList, [$node->{name}, $edge->{name}]; # And remember that this is a parent node push @parentSets, $node->{name}; } } else { print "ERROR: No path found for adding '$table' in\n"; print Dumper($self->{sets}); print "Sucks, eh?\n"; exit; } } } return @joinList; } sub getJoinList { my $self = shift; my @joinList = $self->getJoinListHelper($self->{sets}); # Shift off the first table my $tmp = shift @joinList; my $joinList = $tmp->[0] . ' '; foreach my $table (@joinList) { $joinList .= " LEFT JOIN $table->[0] ON ( $table->[1] ) "; } return $joinList; } =item $set->getQuery() Taking into account current tables, generate a query. Maybe this should be broken into a couple subs? =cut sub getQuery { # In the words of Derik Zoolander, "Who am I?" my $self = shift; # Get the fieldList, joinList, and filter my $fieldList = $self->getFieldList(); my $joinList = $self->getJoinList($self->{sets}); my $filter = $self->{filter}; $filter = "1=1" unless $filter; return "SELECT DISTINCT $fieldList FROM $joinList WHERE $filter"; } =item $set->runQuery() Execute the query (generate it if need be). =cut sub runQuery { my $self = shift; $self->{query} = $self->getQuery(); $self->{sth} = $self->{dbi}->prepare($self->{query}); $self->{sth}->execute(); } =item $set->restart() Clear the current settings for this set -- the query and db handle =cut sub restart { my $self = shift; delete $self->{query}; delete $self->{sth}; } =item $self->filter($whereClause) Sets the where clause =cut sub filter { my $self = shift; $self->{filter} = shift; $self->restart; } =item $self->fetchNext() Return the next tuple from a set, undef if there is none =cut sub fetchNext { my $self = shift; # If our query hasn't been run or has changed we need to run it now $self->runQuery() unless ($self->{query} and $self->{sth}); # First we grab the data row from the database my $row = $self->{sth}->fetchrow_hashref(); if($row) { return undef unless ($row->{id}); my @fields = @{$self->{fields}}; my %fields; foreach my $field (@fields) { my $table; ($table, $field) = split /\./, $field; $fields{$field} = [$table, $row->{$table . '__id'}]; } # Set up a Tuple to hold this stuff my $tuple = new SetDB::Tuple($row, \%fields, $self->{dbi}); my %children = %{$self->{childset}}; foreach my $kid (keys %children) { my $child = $children{$kid}; $children{$kid}->{filter} = $self->{fields}[0] . ".id = " . $row->{$self->{fields}[0] . '__id'}; $tuple->{$kid} = $children{$kid}; # Be sure to restart this kid $tuple->{$kid}->restart(); } return $tuple; } else { return undef; } } =item $set->newEntry() Create a new entry for this set. This will create a record for every entry on the top-level and any connections to higher-levels as necessary. Oh, and it will make all the connections for the entries on this level too. =cut # Three steps to a new entry: # * Create a new record for each table in our level # * Fill in connections between sibling tables # * Fill in values set from filters sub newEntry { my $self = shift; my $ptable = $self->{view}[0]; $self->{dbi}->do("INSERT INTO $ptable SET id = 0"); print "INSERT INTO $ptable SET id = 0\n"; my $newid = $self->{dbi}->{mysql_insertid}; print "New ID! - $newid\n"; my $row; $row->{id} = $newid; $row->{$ptable . '__id'} = $newid; $self->getQuery(); # This just gets the field list my @fields = @{$self->{fields}}; my %fields; foreach my $field (@fields) { my $table; ($table, $field) = split /\./, $field; $fields{$field} = [$table, $row->{$table . '__id'}]; } # Set up a Tuple to hold this stuff my $tuple = new SetDB::Tuple($row, \%fields, $self->{dbi}); my %children = %{$self->{childset}}; foreach my $kid (keys %children) { my $child = $children{$kid}; $children{$kid}->{filter} = $self->{view}[0] . ".id = " . $row->{$self->{view}[0] . '__id'}; $tuple->{$kid} = $children{$kid}; # Be sure to restart this kid $tuple->{$kid}->restart(); } return $tuple; } =back =head1 SEE ALSO SetDB, SetDB::Tuple =head1 AUTHOR Brock Wilcox =head1 COPYRIGHT Copyright (c) 2004 Brock Wilcox . All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # Return 'true' to be a good little module 1;