The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Hermes;

=head1 NAME

Hermes - Cluster information interpreter. Extends Hermes::Range

=head1 SYNOPSIS

 use Hermes;

 my $a = Hermes->new( cache => '/database/file' );

 $a->load( 'foo{2~9}{??==*=={foo,bar}!!baz}.bar' );

 ## ... see base class for other methods ...

=cut
use strict;
use warnings;

use base qw( Hermes::Range );

=head1 QUERY

=head3 cluster

A I<cluster> query is expressed as a tuple of elements, corresponding
to columns of the cache table. See Hermes::DBI::Cache.

Such a query is made when an element is indicated by the I<select> symbol,
and the other three are given as query conditions, expressed by a I<condition>
symbol followed a range expression.  e.g. "{??==*=={foo,bar}!!baz}" may be
translated into the following SQL statement,

 "SELECT col1 FROM $TABLE WHERE col3 IN ('foo','bar') AND col4!='baz'"

=cut
use Hermes::DBI::Cache;

=head3 callabck

A I<callback> query is expressed as a callback symbol followed by the
callback name, and optionally, followed by a query condition.
See Hermes::Call.

e.g. "{%%foo!!bar,baz}" means to get all elements returned by callback
I<foo>, except those indexed by I<bar> and I<baz>.

Naturally, '*' in a query condition means 'any', i.e. I<no condition>.
And a query expression may be I<recursive>.

=cut
use Hermes::Call;

=head1 SYMBOLS

( in addition to those in the base class )

=head3 QUERY

 '??' : select
 '==' : in
 '!!' : not

=cut
$Hermes::Range::SYMBOL{QUERY} =
{
    select => '??', in => '==', 'not' => '!!', call => '%%'
};

sub new
{
    my ( $class, %path ) = splice @_;
    my ( $cache, $callback ) = @path{ qw( cache callback ) };
    my $self = bless Hermes::Range->new(), ref $class || $class;
    $self->{db} = Hermes::DBI::Cache->new( $cache ) if $cache;
    $self->{cb} = Hermes::Call->new( $callback ) if $callback;
    return $self;
}

=head1 METHODS

=head3 db()

Returns cache db object.

=cut
sub db
{
    my $self = shift;
    return $self->{db};
}

=head3 cb()

Returns callback object.

=cut
sub cb
{
    my $self = shift;
    return $self->{cb};
}

=head1 GRAMMAR

( BNF rules additional to those in the base class )

=head3 complex

 '{' [ <expr> | <call> | <cluster> ] '}'

=cut
sub complex
{
    my $self = shift;
    my $token = $self->incr->token( '' );
    my $stage = ! $self->op( QUERY => 0 )
        ? 'expr' : $token eq 'call' ? $token : 'cluster';

    my $result = $self->$stage;

    die unless $self->token( 'close' );
    $self->incr;
    return $result;
}

=head3 call

 <call_sym> <string> <query_cond>

=cut
sub call
{
    my $self = shift;
    my $result = Hermes::KeySet->new();
    my $name = $self->incr->token;
    my $cond = $self->incr->token( 'close' ) ? [] : $self->query_cond;

    return $result unless my $cb = $self->{cb};
    return $result->load( [ map { @$_ } $cb->select( $name => $cond || [] ) ] );
}

=head3 cluster
 
 [ <select_symbol> <query_cond> ** 3 ] |
 [ <query_cond> <select_symbol> <query_cond> ** 2 ] |
 [ <query_cond> ** 2 <select_symbol> <query_cond> ] |
 [ <query_cond> ** 3 <select_symbol> ]

=cut
sub cluster
{
    my $self = shift;
    my ( $result, $select, %cond ) = Hermes::KeySet->new();

    for my $col ( $self->{db}->column() )
    {
        next unless my $cond = $self->query_cond( $col );
        if ( ref $cond ) { $cond{$col} = $cond } else { $select = $col }
    }

    return $result unless my $db = $self->{db};
    return $result->load( [ map { @$_ } $db->select( $select, %cond ) ] );
}

=head3 query_cond

 <condition_symbol> <expr> |
 <condition_symbol> <regex>

=cut
sub query_cond
{
    my ( $self, $col ) = splice @_;

    die unless my $op = $self->op( QUERY => 1 );
    return $op if $op eq 'select';

    my $match = $op eq 'in';

    if ( $self->token( 'regex' ) )
    {
        my $regex = $self->match();
        return
        [
            $match, defined $col ? grep { $_ =~ $regex }
            map { @$_ } $self->{db}->select( $col ) : $regex
        ];
    }

    my $range = $self->expr;
    return $range->has( '*' ) ? undef : [ $match, $range->list ];
}

1;