#!/usr/bin/perl -w

# Natural Language mySQL Interface
# Jamie Olson and Amrit Tuladhar
# Final Project
# CS 357
# Spring 2005

use strict;
use DBI();
use Parser;
use Node;
my ($parser, $sentence_parser, %comparisons, $user, $password, $database, $dbh, @tables, @queries, $query, %orders);
#my ($type, $command, $fields, $table, $condition);
$parser = Parser->new();
# Hash for mapping natural language comparison operators to SQL operators
$comparisons{'greater'} = '>';
$comparisons{'bigger'} = '>';
$comparisons{'less'} = '<';
$comparisons{'smaller'} = '<';
$comparisons{'equal'} = '=';
$comparisons{'starts'} = 'LIKE';
$comparisons{'begins'} = 'LIKE';
$comparisons{'ends'} = 'LIKE';
$comparisons{'terminates'} = 'LIKE';
$comparisons{'contains'} = 'LIKE';

# Hash for mapping natural language order commands to SQL options
$orders{'ascending'} = 'ASC';
$orders{'descending'} = 'DESC';
$orders{'reverse'} = 'DESC';
$orders{'alphabetical'} = 'ASC';

# Load custom grammar and lexicon
$parser->load_grammar("simple-grammar") or die "missing grammar file";
$parser->load_lexicon("simple-lexicon") or die "missing lexicon file";

print "Welcome to the Natural Language MYSQL Interface\n";
# Let the user log into mySQL
print "MYSQL username:";
$user = <>;
chomp($user);
print "MYSQL password:";
$password = <>;
chomp($password);
# Show existing databases
    my @databases = split("\n", `mysql -u $user --password=$password -e 'show databases;'`);
    shift @databases;
    print "The databases you have access to: @databases\n";
    print "Which database would you like to use?\n";
    $database = <>;
    chomp($database);
    $dbh = DBI->connect("DBI:mysql:database=$database;host=localhost",
		       "$user", "$password",
		       {'RaiseError' => 1});

# Show existing tables
my $sth = $dbh->prepare("SHOW TABLES");
$sth->execute();
while(my $ref = $sth->fetchrow_hashref()){
    push(@tables,$ref->{"Tables_in_$database"});
}
print "Tables: @tables\nFor help, type ? or help.\n";

# Show prompt
print "NLSQL> ";
while (<>){
    chomp $_;
    # Does the user want to quit?
    if (lc $_ =~ /quit/i or lc $_ =~ /bye/i or lc $_ =~ /exit/i){
	last;
    
    # Does the user want help?
    }elsif (lc $_ =~ /help/i or lc $_ eq "?"){
	&help();
	
    # Does the user want to see the tables?
    }elsif($_ =~ /\Atable/i or $_ =~ /\Ashow table/i){
	print "Tables: @tables\n";
	
    # Does the user want to run a SQL command directly (command preceded by :)
    }elsif($_ =~ /\A:/){
	s/\A://;
	&execute($_);
	
    # Has the user just pressed enter?
    }elsif(not $_){
	;
	
    # Is it a command?
    }else{
	&process_message($_);
    }
    print "NLSQL> ";
}

# Help
sub help(){
    print "\nNatural Language MySQL Interface:\n";
    print "Just type a request.  I might be able to help you.\n";
    print "Most SELECT and DELETE commands are currently supported.\n";
    print "This includes WHERE and limited ORDER BY capabilities.\n\n";
    print "To see a list of tables in the database, type tables.\n";
    print "To run a mysql command, type : before the command.\n";
    print "To exit, type 'exit' or 'bye' or 'quit'.\n";
}

# Parse the message
sub process_message(){
    my ($sentence) = @_;
    $sentence_parser = $parser->get_new_parse($sentence);
    $sentence_parser->build_productions();
    
    # No parses found
    if(not $sentence_parser->has_parse()){
	print "Sorry, we cannot understand what you're requesting.\n";
    }else{
	my $head = $sentence_parser->get_tree();
	$query = Query->new();
	$query->understand($head);
	&check_query($query);
	my $new_query = $query->get_command();
	if($new_query){
	    print "query: $new_query\n";
	    # Type 1: SELECT: Show the resulting table
	    if($query->{TYPE} == 1){
		$sth = $dbh->prepare($new_query);
		$sth->execute();
		while(my $ref = $sth->fetchrow_hashref()){
		    foreach (keys %{$ref}){
			print "$_: $ref->{$_}     ";
		    }
		    print "\n";
		}
	    }else{
		# Type 2: DELETE: Show how many rows affected
		print ($dbh->do($new_query)," row affected.\n");
	    }
	    
	    push(@queries, $query);
	}
    }
}

# Run a SQL command and show the result
sub execute(){
    my ($query) = @_;
    eval {$dbh->do($query) };
    if($@){
	$sth = $dbh->prepare($query);
	$sth->execute();
	while(my $ref = $sth->fetchrow_hashref()){
	    foreach my $key (keys %{$ref}){
		print "$key: $ref->{$key}\n";
	    }
	    print "\n";
	}
    }
}

# Check if a query is valid (i.e. if the table and fields it mentions exist
sub check_query(){
    my ($query) = @_;
    # Check if table exists
    if ($query->{TABLE}){
	my $typed_table = $query->{TABLE};
	$query->{TABLE} = "";
	foreach my $possible_table (@tables){
	    if ($possible_table =~ /$typed_table/i){
		$query->{TABLE} = $possible_table;
	    }
	}
	if(not $query->{TABLE}){
	    print "I couldn't find the table you're looking for.\n";
	    return "";
	}
    }
    
    # Check if the fields exist
    if ($query->{FIELDS} and $query->{FIELDS} ne '*'){
	$sth = $dbh->prepare("DESCRIBE $query->{TABLE};");
	my @fields;
	$sth->execute();
	while(my $ref = $sth->fetchrow_hashref()){
	    push(@fields,$ref->{'Field'});
	}
	print "Properties: @fields\n";
	my @typed_fields = split(', ', $query->{FIELDS});
	my @new_fields = ();
	$query->{FIELDS} = "";
	foreach my $typed_field (@typed_fields){
	    my $found = 0;
	    foreach my $possible_field (@fields){
		if ($possible_field =~ /$typed_field/i or $typed_field =~ /$possible_field/i){
		    push(@new_fields,$possible_field);
		    $found = 1;
		}
	    }
	    if(not $found){
		print "I couldn't find the field you were looking for: $typed_field.\n";
	    }
	}
	print "fields: @new_fields\n";
	$query->{FIELDS} = join(', ',@new_fields);
    }
    return 1;
    
}

# A Query package that stores information about a query, parsed from natural language input
package Query;
sub new {
    my $class = shift;
    my $self = {};
    $self->{COMMAND} = ""; # The command (SELECT, DELETE)
    $self->{FIELDS} = ""; # The fields
    $self->{TYPE} = ""; # Type of command, SELECT - 1, DELETE - 2
    # We had to make this distinction because SELECT and DELETE return different things and are different in syntax
    $self->{CONDITION} = ""; # The condition (part followed by WHERE)
    $self->{TABLE} = ""; # The table
    $self->{ORDER} = ""; # Is there any ordering information
    bless $self=>$class;
    return $self;
}


# Take a Query object and build a SQL command out of it
sub get_command(){
    my ($self) = @_;
    my $sqlquery = "";
#    print "Using fragments:\n";
 
    if ($self->{TYPE} == 1){
	$sqlquery = $self->{COMMAND};
	$sqlquery = "$sqlquery $self->{FIELDS}" if ($self->{FIELDS});
	$sqlquery = "$sqlquery FROM $self->{TABLE}" if ($self->{TABLE});
	$sqlquery = "$sqlquery WHERE $self->{CONDITION}" if ($self->{CONDITION});
	$sqlquery = "$sqlquery ORDER BY"."$self->{ORDER}" if ($self->{ORDER});
    }elsif ($self->{TYPE} == 2)
    {
	$sqlquery = $self->{COMMAND} if ($self->{COMMAND});
#	$sqlquery = "$sqlquery $self->{FIELDS}" if ($self->{FIELDS}); # Not needed by DELETE
	$sqlquery = "$sqlquery FROM $self->{TABLE}" if ($self->{TABLE});
	$sqlquery = "$sqlquery WHERE $self->{CONDITION}" if ($self->{CONDITION});
    }
    return $sqlquery;
}


# Take the parse for the sentence and "translate" it into SQL
sub understand(){
    my ($self,$head) = @_;
    my $message = "";

    # Find the command
    if($head->{PHRASE} eq 'SHOWME'){
	$self->{COMMAND} = 'SELECT';
	$self->{TYPE} = 1;
	return "SELECT";

    }elsif($head->{PHRASE} eq 'Delete'){
	$self->{COMMAND} = 'DELETE';
	$self->{TYPE} = 2;
	return "DELETE";
	
    }elsif($head->{PHRASE} eq 'Sort'){
	$self->{COMMAND} = 'SELECT';
	$self->{TYPE} = 1;
	return "SELECT";
    
    }elsif($head->{PHRASE} eq 'THING'){
	# The fields found
	my @properties = $self->get_Property($head);
	$message = join(', ',@properties);
	$self->{FIELDS} = $message;
	if(not $message and $self->{COMMAND} ne 'DELETE'){
	    $message =  '*'; 
	    $self->{FIELDS} = $message;
	}

    }elsif($head->{PHRASE} eq 'Table'){
	# The table found
	$message = $head->{CHILDREN}[0]{PHRASE};
	$self->{TABLE} = $message;

    }elsif($head->{PHRASE} eq 'EQUALITY' and $head->{CHILDREN}[0]{PHRASE} eq 'Property'){
	# A condition (of the form "where age is 6") found
	$message = $head->{CHILDREN}[0]{CHILDREN}[0]{PHRASE};
	
	my $compare = "";
	foreach (@{$head->{CHILDREN}[1]{CHILDREN}}){
	    if($_->{PHRASE} eq 'Compare'){
		$compare = $_->{CHILDREN}[0]{PHRASE};
	    }
	}
	if($comparisons{$compare}){
	    $message = "$message $comparisons{$compare}";
	}else{
	    $message = "$message =";
	}
	my $value = $head->{CHILDREN}[2]{CHILDREN}[0]{PHRASE};
	$value = "$value%" if($compare eq 'starts' or $compare eq 'begins' or $compare eq 'contains');
	$value = '%'.$value if($compare eq 'ends' or $compare eq 'terminates' or $compare eq 'contains');
	$message = "$message \'$value\'";
	if ($self->{CONDITION}){
	    $self->{CONDITION} = "$self->{CONDITION} $message";
	}else{
	    $self->{CONDITION} = "$message";
	}

    }elsif($head->{PHRASE} eq 'conj'){
	# A conjunction found
	$message = uc $head->{CHILDREN}[0]{PHRASE};
	$self->{CONDITION} = "$self->{CONDITION} $message";

    }elsif($head->{PHRASE} eq 'PROPERTYVALUE' and $head->{CHILDREN}[0]{PHRASE} ne 'PROPERTYVALUE'){
	# A condition (of the form "that has the age 6") found
	$message = $head->{CHILDREN}[0]{CHILDREN}[0]{PHRASE};
	if(not defined $head->{CHILDREN}[2]){
	    my $value = $head->{CHILDREN}[1]{CHILDREN}[0]{PHRASE};
	    $message = "$message = \'$value\'";
	}else{
	    my $compare = "";
	    foreach (@{$head->{CHILDREN}[1]{CHILDREN}}){
		if($_->{PHRASE} eq 'Compare'){
		    $compare = $_->{CHILDREN}[0]{PHRASE};
		}
	    }
	    if($comparisons{$compare}){
		$message = "$message $comparisons{$compare}";
	    }else{
		$message = "$message =";
	    }
	    my $value = $head->{CHILDREN}[2]{CHILDREN}[0]{PHRASE};
	    $value = "$value%" if($compare eq 'starts' or $compare eq 'begins' or $compare eq 'contains');
	    $value = '%'.$value if($compare eq 'ends' or $compare eq 'terminates' or $compare eq 'contains');
	    $message = "$message \'$value\'";
	}
	if ($self->{CONDITION}){
	    $self->{CONDITION} = "$self->{CONDITION} $message";
	}else{
	    $self->{CONDITION} = "$message";
	}
    
    }elsif($head->{PHRASE} eq 'ORDER'){
	# Ordering information (e.g. "in ascending order" or "in reverse order") found
	foreach (@{$head->{CHILDREN}}){
	    if ($_->{PHRASE} eq 'Property'){
		$message = "$message $_->{CHILDREN}[0]{PHRASE}";
	    }
	}
	my $orderBy = "";
	foreach (@{$head->{CHILDREN}}){
	    if ($_->{PHRASE} eq 'Order'){
		if ($orders{$_->{CHILDREN}[0]{PHRASE}}){
		    $orderBy = $orders{$_->{CHILDREN}[0]{PHRASE}};
		}
	    }
	}
        $orderBy = 'ASC' unless ($orderBy);
	$message = "$message $orderBy"; 
	$self->{ORDER} = $message;    

    }else{
	# If none of the above was true, run the "translator" on all children
	foreach (@{$head->{CHILDREN}}){
	    $self->understand($_);
	}
    }
}


# Get the properties from a PROPERTY item in the parse
sub get_Property(){
    my ($self, $node) = @_;
    my @returnlist;
    if($node->{PHRASE} eq 'Property'){
	return $node->{CHILDREN}[0]{PHRASE};
    }else{
	foreach (@{$node->{CHILDREN}}){
	    push(@returnlist,$self->get_Property($_));
	}
    }
    return @returnlist;
}

