/usr/local/CPAN/Xmldoom/Xmldoom/Criteria.pm



package Xmldoom::Criteria;

use Xmldoom::Criteria::Search;
use Xmldoom::Criteria::ExplicitJoinVisitor;
use Xmldoom::Threads;
use DBIx::Romani::Query::SQL::Column;
use strict;

use Data::Dumper;

# Search types
our $AND = 'AND';
our $OR  = 'OR';

# comparison types
our $EQUAL         = '=';
our $NOT_EQUAL     = '<>';
our $GREATER_THAN  = '>';
our $GREATER_EQUAL = '>=';
our $LESS_THAN     = '<';
our $LESS_EQUAL    = '<=';
our $LIKE          = 'LIKE';
our $NOT_LIKE      = 'NOT LIKE';
our $ILIKE         = 'ILIKE';
our $NOT_ILIKE     = 'NOT ILIKE';
our $BETWEEN       = 'BETWEEN';
our $IN            = 'IN';
our $NOT_IN        = 'NOT IN';
our $IS_NULL       = 'IS NULL';
our $IS_NOT_NULL   = 'IS NOT NULL';

sub new
{
	my $class = shift;
	my $args  = shift;

	my $parent;
	my $shared;

	if ( ref($args) eq 'HASH' )
	{
		$parent = $args->{parent};
		$shared = $args->{shared};
	}
	else
	{
		$parent = $args;
	}
	
	my $self = {
		parent   => $parent,
		search   => Xmldoom::Criteria::Search->new({ type => 'AND' }),
		order_by => [ ],
		group_by => [ ],
		limit    => undef,
		offset   => undef,
	};

	bless $self, $class;

	# we want to move into shared memory as soon as possible
	$self = Xmldoom::Threads::make_shared($self, $shared);

	return $self;
}

sub get_parent   { return shift->{parent}; }
sub get_type     { return "AND"; }
sub get_order_by { return shift->{order_by}; }
sub get_group_by { return shift->{order_by}; }
sub get_limit    { return shift->{limit}; }
sub get_offset   { return shift->{offset}; }

sub set_parent
{
	my ($self, $parent) = @_;
	$self->{parent} = $parent;
}

sub set_limit
{
	my ($self, $limit, $offset) = @_;
	$self->{limit}  = $limit;
	$self->{offset} = $offset;
}

sub add
{
	my $self = shift;
	$self->{search}->add( @_ );
}

sub add_attr
{
	my $self = shift;
	$self->{search}->add_attr( @_ );
}

sub add_prop
{
	my $self = shift;
	$self->{search}->add_prop( @_ );
}

sub join_attr
{
	my $self = shift;
	$self->{search}->join_attr( @_ );
}

sub join_prop
{
	my $self = shift;
	$self->{search}->join_prop( @_ );
}

sub add_order_by_attr
{
	my ($self, $attr, $dir) = @_;
	my ($table_name, $column) = split '/', $attr;

	my %order_by = (
		attr  => {
			table_name => $table_name,
			column     => $column,
		},
		value => {
			dir => $dir,
		}
	);

	push @{$self->{order_by}}, \%order_by;
}

sub add_order_by_prop
{
	my ($self, $prop, $dir) = @_;
	my ($object_name, $prop_name) = split '/', $prop;

	my %order_by = (
		prop  => {
			object_name => $object_name,
			prop_name   => $prop_name,
		},
		value => {
			dir => $dir,
		}
	);

	push @{$self->{order_by}}, \%order_by;
}

# A convenience alias.
sub add_order_by
{
	my $self = shift;
	$self->add_order_by_prop(@_);
}

sub add_group_by_attr
{
	my ($self, $attr) = @_;
	my ($table_name, $column) = split '/', $attr;

	my %group_by = (
		attr  => {
			table_name => $table_name,
			column     => $column,
		}
	);

	push @{$self->{group_by}}, \%group_by;
}

sub add_group_by_prop
{
	my ($self, $prop) = @_;
	my ($object_name, $prop_name) = split '/', $prop;

	my %group_by = (
		prop  => {
			object_name => $object_name,
			prop_name   => $prop_name,
		}
	);

	push @{$self->{group_by}}, \%group_by;
}

# A convenience alias.
sub add_group_by
{
	my $self = shift;
	$self->add_group_by_prop(@_);
}

sub _setup_query
{
	my ($self, $database, $query) = @_;

	my $search;

	if ( defined $self->{parent} )
	{
		$search = $self->{search}->clone();

		# add the values of its primary keys to the criteria
		foreach my $col ( @{$self->{parent}->_get_definition()->get_table()->get_columns()} )
		{
			if ( $col->{primary_key} )
			{
				my $attr_name = join '/', $self->{parent}->_get_definition()->get_table_name(), $col->{name};
				# we use key instead of the attr values, in case they were changed, we
				# should still query against the current database value.
				$search->add_attr( $attr_name, $self->{parent}->{key}->{$col->{name}} );
			}
		}
	}
	else
	{
		$search = $self->{search};
	}

	# add the from stuff
	foreach my $table_name ( @{$search->get_tables($database)} )
	{
		$query->add_from( $table_name );
	}

	# build the where clause
	$query->set_where( $search->generate($database) );

	# set the limit and offset
	$query->set_limit( $self->{limit}, $self->{offset} );
}

sub _apply_order_by_to_query
{
	my ($self, $database, $query) = @_;

	foreach my $order_by ( @{$self->{order_by}} )
	{
		if ( defined $order_by->{attr} )
		{
			# add the table to the query
			$query->add_from( $order_by->{attr}->{table_name} );
			
			# add, yo.
			my $value = DBIx::Romani::Query::SQL::Column->new({
				table => $order_by->{attr}->{table_name},
				name  => $order_by->{attr}->{column}
			});
			$query->add_order_by({ value => $value, dir => $order_by->{value}->{dir} });
		}
		elsif ( defined $order_by->{prop} )
		{
			my $object = $database->get_object( $order_by->{prop}->{object_name} );
			if ( not defined $object )
			{
				die "Unable to find object '$order_by->{prop}->{object_name}' in order_by";
			}

			my $prop   = $object->get_property( $order_by->{prop}->{prop_name} );
			if ( not defined $prop )
			{
				die "Unable to find property '$order_by->{prop}->{prop_name}' in object '$order_by->{prop}->{prop_name}' in order_by";
			}

			# TODO: this should really "visit" the returned lval to determine what
			# tables this includes ...
			$query->add_from( $object->get_table_name() );

			foreach my $lval ( @{$prop->get_query_lval()} )
			{
				$query->add_order_by({ value => $lval, dir => $order_by->{value}->{dir} });
			}
		}
	}
}

# TODO: This was just copied from _apply_order_by_to_query.  These two should be 
# merged if possible somehow.
sub _apply_group_by_to_query
{
	my ($self, $database, $query) = @_;

	foreach my $group_by ( @{$self->{group_by}} )
	{
		if ( defined $group_by->{attr} )
		{
			# add the table to the query
			$query->add_from( $group_by->{attr}->{table_name} );
			
			# add, yo.
			my $value = DBIx::Romani::Query::SQL::Column->new({
				table => $group_by->{attr}->{table_name},
				name  => $group_by->{attr}->{column}
			});
			$query->add_group_by( $value );
		}
		elsif ( defined $group_by->{prop} )
		{
			my $object = $database->get_object( $group_by->{prop}->{object_name} );
			if ( not defined $object )
			{
				die "Unable to find object '$group_by->{prop}->{object_name}' in group_by";
			}

			my $prop   = $object->get_property( $group_by->{prop}->{prop_name} );
			if ( not defined $prop )
			{
				die "Unable to find property '$group_by->{prop}->{prop_name}' in object '$group_by->{prop}->{prop_name}' in group_by";
			}

			# TODO: this should really "visit" the returned lval to determine what
			# tables this includes ...
			$query->add_from( $object->get_table_name() );

			foreach my $lval ( @{$prop->get_query_lval()} )
			{
				$query->add_group_by( $lval );
			}
		}
	}
}

sub _join_to_tables
{
	my ($self, $database, $query) = @_;

	# if there is only one table of this query then we don't have to worry at all.
	if ( scalar @{$query->get_from()} == 1 )
	{
		return;
	}

	# get a list of exiplicit joins on the existings query
	my $visitor = Xmldoom::Criteria::ExplicitJoinVisitor->new();
	my $explicit_joins;
	if ( $query->get_where() )
	{
		$explicit_joins = $query->get_where()->visit( $visitor );
	}
	#print Dumper $explicit_joins;

	my $where       = DBIx::Romani::Query::Where->new();
	my @tables      = @{$query->get_from()};
	my $main_table  = shift @tables;
	my %joined_hash = ( $main_table => 1 );
	my %tables_hash = map { $_ => 1 } @tables;

	while( scalar keys %tables_hash > 0 )
	{
		my $joined = 0;

		# go through the list of unconnected tables
		foreach my $table_name ( keys %tables_hash )
		{
			my $links = [ ];
			my $new_link;

			# look for connections to already connected tables
			foreach my $other_table_name ( keys %joined_hash )
			{
				$links = [ @$links, @{$database->find_links( $table_name, $other_table_name )} ];
			}

			# reduce the overlapping links
			$links = Xmldoom::Definition::Link::reduce_shortest( $links );

			# if there are no links, then we look to see if an explicit link exists
			if ( scalar @$links == 0 )
			{
				foreach my $explicit ( @$explicit_joins )
				{
					if ( $explicit->{local_table} eq $table_name and
						 $joined_hash{$explicit->{foreign_table}} )
					{
						# cool!  We've got an explicit link to one of the joined tables.
						$joined = 1;
						last;
					}
				}
			}
			elsif ( scalar @$links > 1 )
			{
				# attempt to disambiguate the multiple links using the explicit joins.

				LINK: foreach my $link ( @$links )
				{
					my $is_already_explicit = 1;

					# check to see if all of the foreign keys in this link, are covered by
					# a pre-existing explicit link.
					foreach my $fn ( @{$link->get_foreign_keys()} )
					{
						foreach my $ref ( @{$fn->get_column_names()} )
						{
							my $has_this_one = 0;

							foreach my $explicit ( @$explicit_joins )
							{
								if ( $ref->{local_table} eq $explicit->{local_table} and
									 $ref->{local_column} eq $explicit->{local_column} and
									 $ref->{foreign_table} eq $explicit->{foreign_table} and
									 $ref->{foreign_column} eq $explicit->{foreign_column} )
								{
									$has_this_one = 1;
								}
							}

							if ( not $has_this_one )
							{
								$is_already_explicit = 0;
								next LINK;
							}
						}
					}

					if ( $is_already_explicit )
					{
						# we are explicitly joined already, yo!
						$joined = 1;
						last;
					}
				}

				if ( not $joined )
				{
					# This is an error!  A serious error, yo!
					die "There are multiple ways in which these tables could be linked, so an explicit join must be used to select one of them."

					#print STDERR "WARNING: There are multiple ways in which these tables could be linked, but no explicit join was given so the first available link was chosen.\n";
					#$new_link = $links->[0];
				}
			}
			else
			{
				# We only have the one possible link, so attempt to automatically join on that.
				$new_link = $links->[0];
			}
			
			if ( defined $new_link )
			{
				# join the two tables
				foreach my $fn ( @{$new_link->get_foreign_keys()} )
				{
					foreach my $ref ( @{$fn->get_column_names()} )
					{
						my $is_already_explicit = 0;

						foreach my $explicit ( @$explicit_joins )
						{
							if ( $ref->{local_table} eq $explicit->{local_table} and
								 $ref->{local_column} eq $explicit->{local_column} and
								 $ref->{foreign_table} eq $explicit->{foreign_table} and
								 $ref->{foreign_column} eq $explicit->{foreign_column} )
							{
								$is_already_explicit = 1;
							}
						}

						if ( not $is_already_explicit )
						{
							my $join = DBIx::Romani::Query::Comparison->new();

							# NOTE: We do this in reverse than expected order because we looping
							# essentially backwards.  The first item on the list of foriegn tables
							# is thought to be our master table...

							$join->add( DBIx::Romani::Query::SQL::Column->new( $ref->{foreign_table}, $ref->{foreign_column} ) );
							$join->add( DBIx::Romani::Query::SQL::Column->new( $ref->{local_table}, $ref->{local_column} ) );
							$where->add( $join );

							# add to the from list if we are dealing with a many-to-many link
							$query->add_from( $ref->{foreign_table} );

							# also, remove this from the unjoined tables and add them to the 
							# the joined list!
							$joined_hash{$ref->{foreign_table}} = 1;
							delete $tables_hash{$ref->{foreign_table}};
						}
					}
				}

				$joined = 1;
			}

			if ( $joined )
			{
				# we mark this table as joined and restart
				$joined_hash{$table_name} = 1;
				delete $tables_hash{$table_name};
				last;
			}
		}

		if ( not $joined )
		{
			die "Unable to join the following tables: " . join(', ', keys %tables_hash);
		}
	}

	# merge the old where statement with the connection one
	if ( scalar @{$where->get_values()} > 0 )
	{
		my $old_where = $query->get_where();
		if ( $old_where )
		{
			$where->add ( $old_where );
		}
		$query->set_where( $where );
	}
}

sub generate_query_for_object
{
	my ($self, $database, $object_name) = @_;

	my $definition = $database->get_object( $object_name );
	my $query      = $definition->get_select_query()->clone();

	# setup the query
	$self->_setup_query( $database, $query );

	# add the order by
	$self->_apply_order_by_to_query( $database, $query );
	
	# add the group by
	$self->_apply_group_by_to_query( $database, $query );

	# make sure all the appropriate connections exist
	$self->_join_to_tables( $database, $query );

	return $query;
}

sub generate_query_for_object_count
{
	my ($self, $database, $object_name) = @_;

	my $definition = $database->get_object( $object_name );

	my $query      = $definition->get_select_query()->clone();
	my $table      = $definition->get_table();
	my $table_name = $definition->get_table_name();

	# make a query for COUNT() of the objects first primary key
	$query->clear_result();
	foreach my $column ( @{$table->get_columns()} )
	{
		if ( $column->{primary_key} )
		{
			my $count = DBIx::Romani::Query::Function::Count->new();
			$count->add( DBIx::Romani::Query::SQL::Column->new( $table_name, $column->{name} ) );
			$query->add_result( $count, 'count' );

			# we're cool
			last;
		}
	}
	
	# setup the query
	$self->_setup_query( $database, $query );

	# make sure all the appropriate connections exist
	$self->_join_to_tables( $database, $query );

	# we don't want to limit or offset on a count query
	$query->clear_limit();

	return $query;
}

sub generate_query_for_attrs
{
	my ($self, $database, $attrs) = (shift, shift, shift);

	# we can put on the list, or use an array hash
	if ( ref($attrs) ne 'ARRAY' )
	{
		$attrs = [ $attrs, @_ ];
	}

	my $query = DBIx::Romani::Query::Select->new();

	foreach my $attr ( @$attrs )
	{
		my ($table_name, $column) = split '/', $attr;

		# add the column to the result list
		$query->add_result( DBIx::Romani::Query::SQL::Column->new( $table_name, $column ) );

		# add to the table list
		$query->add_from( $table_name );
	}

	# setup the query
	$self->_setup_query( $database, $query );

	# add the order by stuff
	$self->_apply_order_by_to_query( $database, $query );

	# add the group by
	$self->_apply_group_by_to_query( $database, $query );

	# make sure all the appropriate connections exist
	$self->_join_to_tables( $database, $query );

	return $query;
}

sub generate_description
{
	my $self = shift;

	return $self->{search}->generate_description( @_ );
}

sub clone
{
	my $self   = shift;
	my $parent = shift;

	if ( not defined $parent )
	{
		$parent = $self->get_parent();
	}
	
	my $criteria = Xmldoom::Criteria->new( $parent );

	# copy all the deep information
	$criteria->{search} = $self->{search}->clone();
	foreach my $order_by ( @{$self->get_order_by()} )
	{
		push @{$criteria->{order_by}}, $order_by;
	}
	foreach my $group_by ( @{$self->get_group_by()} )
	{
		push @{$criteria->{group_by}}, $group_by;
	}

	# shallow mallow
	$criteria->set_limit( $self->get_limit(), $self->get_offset() );

	return $criteria;
}

1;