#/usr/bin/perl

# SquishQL->SQL converter. Pass in a Squish query on STDIN or in
#   a file via the commandline.

use Parse::RecDescent;
use warnings;
use strict;

# Grammar adapted from http://swordfish.rdfweb.org/rdfquery/squish-bnf.html
#   by Libby Miller <libby.miller (at) bristol.ac.uk>
#
# Example SquishQL queries can be found there, as well.

my $Grammar = <<'EOG';
{ my %using }

start: Query /\s*\Z/
    { $return = $item{Query} }
    | <error>

Query: SelectClause (FromClause)(?) TriplePatternClause
    (ConstraintClause)(?) (UsingClause)(?)
    { $return = {
	select  => $item[1],
	from    => $item[2],
	where   => $item[3],
	const   => $item[4],
	using	=> \%using
      }; 
    }

SelectClause: /SELECT/i VarList
    { $return = $item{VarList} }

FromClause: /FROM/i UriList
    { $return = $item{UriList} }

TriplePatternClause: /WHERE/i TriplePatternList
    { $return = $item{TriplePatternList} }

ConstraintClause: /AND/i ConstraintList
    { $return = $item{ConstraintList} }

UsingClause: /USING/i (ForList)(s)
    { $return = $item{ForList} }

TriplePatternList: TriplePattern (TriplePattern)(s?)
    { $return = [$item[1], $item[2] ? @{$item[2]} : ()] }

TriplePattern: '(' VarOrLiteral VarOrLiteral VarOrLiteral ')'
    { $return = [@item[2..4]] }

VarOrLiteral: Var
    | Literal

Var: '?' Identifier
    { $return = "?$item{Identifier}" }

VarList: Var ((',')(?) Var)(s?)
    { $return = [$item[1], $item[2] ? @{$item[2]} : ()] }

UriList: UriLiteral ((",")(?) UriLiteral)(s?)

ConstraintList: Expression (/AND/i Expression)(s?)

ForList: Identifier /FOR/i UriLiteral
    { $using{$item{Identifier}} = $item{UriLiteral} }

Expression: Var SomeFunction
    { $return = [@item] }

SomeFunction: (NumExpression
    | StringExpression)(s)

NumExpression: ('>'
    | '<'
    | '=='
    | '='
    | '!='
    | '<='
    | '>=') NumericLiteral

StringExpression: (/like/i
    | /ne/i
    | /eq/i
    | '~') Literal

Literal: TextLiteral
    | UriLiteral
    | NumericLiteral

NumericLiteral: /\d+/
    | /\d+\.\d+/

UriLiteral: /[A-Za-z]+:[\w:.#\/-]+/

TextLiteral: /'\w+'/

Identifier: /[A-Za-z]\w*/
EOG

# %Name maps internal names to table and column
# names in your particular RDF database. Really
# should be passed as an argument to triple_sql()
my %Name = (
    subj    => "subj",
    obj	    => "obj",
    pred    => "pred",
    id	    => "seq",
    data    => "data",
    triple  => "triple",
    node    => "node"
);

# Ordering of subject/predicate/object in our clauses.
my @Triple = qw( pred subj obj );

# triple_sql derived from perl example at
#   http://www.picdiary.com/triplequerying/triple_pl.txt
#   by Matt Biddulph <matt (at) picdiary.com>

sub triple_sql {
    my ($selected, $clauses, $using) = @_;
    my (%vars, @where);
    my ($n, $r) = (1, 1);

    # Build up a list of clauses, and find bound variables.
    foreach my $clause (@$clauses) {
	foreach my $p (0 .. 2) {
	    if ($clause->[$p] =~ /^\?(.+)/o) {
		# Found a bound variable.
		push @{$vars{$1}}, "node$n";
	    } else {
		my $literal = $clause->[$p];

		# Substitute "USING" namespace shorthand if
		#     this is a predicate node.
		$literal =~ s/^(\w+):/$using->{$1}/o
		    if $using and $Triple[$p] eq "pred";

		# Match against a literal string.
		push @where, "node$n.$Name{data} = '$literal'";
	    }

	    # Link the node to the tuple.
	    push @where, "node$n.$Name{id} = rel$r.$Name{$Triple[$p]}";
	    $n++;
        }
        $r++;
    }

    # Link all of the bound variables.
    for my $bind (values %vars) {
        for(my $i = 0; $i < $#$bind; $i++) {
            push(@where, "$bind->[$i].$Name{id} = $bind->[$i+1].$Name{id}");
        }
    }

    # Build the column & table lists, then assemble the SQL.
    my (@tables, @select);
    push (@select, "$vars{$_}[0].$Name{data} AS $_") for map(/\?(.+)/g, @$selected);
    push @tables, "$Name{node} node$_" for (1 .. $n);
    push @tables, "$Name{triple} rel$_" for (1 .. $r);

    my $sql = "SELECT DISTINCT ";
    $sql .= join(",",@select);
    $sql .= " \nFROM ";
    $sql .= join(",",@tables);
    $sql .= " \nWHERE ";
    $sql .= join(" \nAND ",@where);
    return $sql;
}

my $parser = Parse::RecDescent->new($Grammar);
my $data   = $parser->start(join("", <>));

print triple_sql( @$data{ "select", "where", "using" } );
print "\n";
