package Xmldoom::Definition::Database;
use Xmldoom::Definition::Object;
use Xmldoom::Definition::SAXHandler;
use Xmldoom::Definition::LinkTree;
use Xmldoom::Definition::Link;
use Xmldoom::Threads;
use Exception::Class::TryCatch;
use XML::SAX::ParserFactory;
use strict;
use Data::Dumper;
sub new
{
my $class = shift;
my $args = shift;
my $schema;
if ( ref($args) eq 'HASH' )
{
$schema = $args->{schema};
}
else
{
$schema = $args;
}
my $self = {
schema => $schema,
objects => { },
real_links => Xmldoom::Definition::LinkTree->new(),
inferred_links => Xmldoom::Definition::LinkTree->new(),
many_to_many_links => Xmldoom::Definition::LinkTree->new(),
connection_factory => undef,
};
# go through and add all of the real links from the schema
while ( my ($table_name, $table) = each %{$self->{schema}->get_tables()} )
{
foreach my $fkey ( @{$table->get_foreign_keys()} )
{
$self->{real_links}->add_link( Xmldoom::Definition::Link->new($fkey) );
}
}
bless $self, $class;
return Xmldoom::Threads::make_shared($self, $args->{shared});
}
sub get_connection_factory { return shift->{connection_factory}; }
sub get_schema { return shift->{schema}; }
sub get_tables { return shift->{schema}->get_tables; }
sub get_table
{
my ($self, $name) = @_;
return $self->{schema}->get_table($name);
}
sub has_table
{
my ($self, $name) = @_;
return $self->{schema}->has_table($name);
}
sub get_objects { return shift->{objects}; }
sub get_object
{
my ($self, $name) = @_;
if ( not defined $self->{objects}->{$name} )
{
die "Unknown object named '$name'";
}
return $self->{objects}->{$name};
}
sub has_object
{
my ($self, $name) = @_;
return defined $self->{objects}->{$name};
}
sub set_connection_factory
{
my ($self, $factory) = @_;
$self->{connection_factory} = $factory;
}
sub create_db_connection
{
return shift->get_connection_factory()->create();
}
sub create_object
{
my ($self, $object_name, $table_name) = @_;
if ( defined $self->{objects}->{$object_name} )
{
die "Object definition for \"$object_name\" already added.";
}
# add and return the object definition
my $object = Xmldoom::Definition::Object->new({
definition => $self,
object_name => $object_name,
table_name => $table_name,
shared => Xmldoom::Threads::is_shared($self)
});
$self->{objects}->{$object_name} = $object;
return $object;
}
sub find_links
{
my ($self, $table1_name, $table2_name) = @_;
if ( not $self->has_table($table1_name) or not $self->has_table($table2_name) )
{
die "Cannot find connections between one or more non-existant tables";
}
my $links;
# NOTE: In case anyone is wondering, the links are seperated into three different
# trees inorder to seperate which pools of links is used for calculating the links
# in another pool. Specifically, when we caclulate the inferred links we want to
# draw *only* on real links, and not other inferred links or many to many links.
# Similarily, when we calculate many to many links we only want to consider real links
# and inferred links but not other many to many links.
# check stored real links
$links = $self->{real_links}->get_links($table1_name, $table2_name);
if ( defined $links )
{
return $links;
}
# check stored inferred links
$links = $self->_find_inferred_links($table1_name, $table2_name);
if ( defined $links )
{
return $links;
}
# check stored many-to-many links
$links = $self->_find_many_to_many_links($table1_name, $table2_name);
if ( defined $links )
{
return $links;
}
# attempt to find new inferred links
return [];
}
sub _find_inferred_links
{
my ($self, $table1_name, $table2_name) = @_;
# first check to see if there is a cached link available
my $cached_links = $self->{inferred_links}->get_links($table1_name, $table2_name);
if ( defined $cached_links )
{
return $cached_links;
}
my @ret;
# now, attempt to find an inferred link, begginning by grabing a list of all the tables
# that this table is links to.
my $link_hash = $self->{real_links}->get_links($table1_name);
while ( my ($inter_table, $links) = each %$link_hash )
{
for my $link ( @$links )
{
# here we check to see if there are any links and the linked table, to the desired
# table by way of the columns specified at the end of the original link.
my $other_links = $self->{real_links}->get_links($inter_table, $table2_name, $link->get_end_column_names());
# multiple links are ok --- they just mean that there are multiple inferred links,
# with the associated problems handled elsewhere, just as they would have to be
# with the relevent real links.
foreach my $other_link ( @$other_links )
{
if ( defined $other_link )
{
my $inferred_link = Xmldoom::Definition::Link->new(
Xmldoom::Schema::ForeignKey->new({
parent => $self->get_schema()->get_table($table1_name),
reference_table => $table2_name,
local_columns => $link->get_start_column_names(),
foreign_columns => $other_link->get_end_column_names()
})
);
# cache the result for later
$self->{inferred_links}->add_link( $link );
push @ret, $inferred_link;
}
}
}
}
if ( scalar @ret > 0 )
{
return \@ret;
}
return undef;
}
sub _find_many_to_many_links
{
my ($self, $table1_name, $table2_name) = @_;
# first check to see if there is a cached link available
my $cached_links = $self->{many_to_many_links}->get_links($table1_name, $table2_name);
if ( defined $cached_links )
{
return $cached_links;
}
my @ret;
# get all the connections to other tables from the real links and the inferred links
# while purposely not checking the many-to-many links which would just create problems.
my $link_hash = { };
my $temp;
if ( defined ($temp = $self->{real_links}->get_links($table1_name)) )
{
$link_hash = { %$link_hash, %$temp };
}
if ( defined ($temp = $self->{inferred_links}->get_links($table1_name)) )
{
$link_hash = { %$link_hash, %$temp };
}
# we loop through simply looking for a table we are linked to which is also linked to
# the desired table. We don't have to check to see if this "inferred" or truely "many
# to many" because we know that the inferred keys will be checked first. This has the
# weakness of only working for single table "jumps," but any type of recursition scares
# me just now.
while ( my ($inter_table, $links) = each %$link_hash )
{
for my $link ( @$links )
{
my $other_links = $self->{real_links}->get_links($inter_table, $table2_name);
# return multiple links as we find them, to be dealt with by the calling code.
foreach my $other_link ( @$other_links )
{
if ( defined $other_link )
{
my $many_to_many_link = Xmldoom::Definition::Link->new([
$link->get_foreign_key(),
$other_link->get_foreign_key()
]);
# cache the result for later
$self->{many_to_many_links}->add_link( $many_to_many_link );
push @ret, $many_to_many_link;
}
}
}
}
if ( scalar @ret > 0 )
{
return \@ret;
}
return undef;
}
sub parse_object_string
{
my ($self, $input) = @_;
# build the parser
my $handler = Xmldoom::Definition::SAXHandler->new( $self );
my $parser = XML::SAX::ParserFactory->parser(Handler => $handler);
# phase 1 -- Create the objects and attach to respective tables
$parser->parse_string($input);
# phase 2 -- Actually add all the properties to the objects
$parser->parse_string($input);
}
sub parse_object_uri
{
my ($self, $uri) = @_;
# build the parser
my $handler = Xmldoom::Definition::SAXHandler->new( $self );
my $parser = XML::SAX::ParserFactory->parser(Handler => $handler);
# phase 1 -- Create the objects and attach to respective tables
$parser->parse_uri($uri);
# phase 2 -- Actually add all the properties to the objects
$parser->parse_uri($uri);
}
sub SearchRS
{
my $self = shift;
my $criteria = shift;
my $query = $criteria->generate_query_for_attrs( $self, @_ );
my $conn;
my $rs;
# connect and query
try eval
{
$conn = $self->create_db_connection();
#printf STDERR "Search(): %s\n", $conn->generate_sql($query);
$rs = $conn->prepare( $query )->execute();
};
catch my $err;
if ( $err )
{
$conn->disconnect() if defined $conn;
$err->rethrow();
}
return $rs;
}
sub Search
{
my $class = shift;
my $rs = $class->SearchRS( @_ );
my @ret;
# unravel our result set
while ( $rs->next() )
{
push @ret, $rs->get_row();
}
return wantarray ? @ret : \@ret;
}
#sub DESTROY
#{
# my $self = shift;
#
# if ( $self->get_dbh() )
# {
# $self->get_dbh()->disconnect();
# $self->set_dbh( undef );
# }
#}
1;