package MySQL::Benchmark::Query;
use 5.006;
use strict;
use warnings FATAL => 'all';

=head1 NAME

MySQL::Benchmark::Query - an MySQL::Benchmark randomized query abstraction

=head1 SYNOPSIS

    my $query = MySQL::Benchmark::Query->new($yaml_query);
    my $sth   = eval {
        # Returns just the parametrized SQL code.
        $dbh->prepare( $query->sql );
        # Returns randomized parameters accordingly to the YAML description for every call.
        $sth->execute( $query->parameters );
    };
    die if $@;

=head1 DESCRIPTION

=head1 QUERY DESCRIPTION

The Query Description is a data-structure (usually generated from a
configuration file) containing three main elements: an ID String, the
parametrised SQL code to execute (with usual SQL-style placeholders), and an
array of rules to generate arguments.

=head2 ID String (key C<id>)

It's just a string. By convention we'll use the Query-ID generated by
L<pt-query-digest>, which is supposed to be unique and is easy to handle. This
will be the identifier key used on the benchmark reports.

=head2 Query Weight (key C<weight>)

The query weight is just an integer. The higher the integer the more a given
query will appear in the workload. The exact proportion will be calculated by
adding up all Query Weight values in the list of queries, and then normalising
it to a value between 1 and 100, in such a way that every query has at least
weight 1 and in total the sum of all normalised weights is 100.

=head2 Parametrised SQL Code (key C<sql>)

Just plain old SQL with placeholders. Nothing special here.

=head2 The Arguments Generation Array (key C<arguments>)

This is where the magic takes place. This array must contain a list of hashes,
every one of them containing the key C<type> and several other keys depending
on the type. This type and keys will be used to put together randomised
arugments for the queries, that can be returned in order or obtained as an
ordered array of subroutines.

Below we'll desribe each key and it's mandatory parameters.

=head2 Argument Types

Type tells the Query class what is expected from this SQL parameter. Available
types are:

=head3 C<constant>

The C<constant> type evaluates to the same parameter every time. This is the
simplest (and probably the least useful) type. When specifying the C<constant>
type, please provide:

=over 4

=item C<value>

The desired value for the constant.

=item C<quantity> (optional, default: 1)

How many values every call will produce (returned as a list).

=back

Example:

    {   id        => 'Example Query with Constant Parameter',
        weight    => 35,
        sql       => 'SELECT id FROM some_table WHERE idx_val = ?',
        arguments => [ { type => 'constant', value => 10 } ]
    }

=head3 C<list>

The C<list> type evaluates to the list provided every time.

=over 4 

=item C<values> 

The ordered list of values to be returned by this parameter.

=back

Example:

    {   id        => 'Example Query with List Parameter',
        weight    => 35,
        sql       => 'SELECT * FROM some_table WHERE idx_val IN ( ?, ?, ?, ?, ? )',
        arguments => [ { type => 'list', values => [ 1, 2, 3, 4, 5 ] } ]
    }

=head3 C<random_integer>

The C<random_integer> type evaluates to a random value from a range provided.
Additional parameters are:

=over 4

=item C<min>

The floor integer in the range (inclusive).

=item C<max>

The ceiling integer in the range (inclusive).

=item C<quantity> (optional. default: 1)

How many values every call will produce, returned as a list.

=back

Example:

    {   id        => 'Random Integer Example Query',
        weight    => 35,
        sql       => 'SELECT * FROM some_table WHERE idx_val = ?',
        arguments => [ { type => 'random_integer', min => 1, max => 10 } ]
    }

=head3 C<random_from_list>

The C<random_from_list> type evaluates to a random value choosen from a list of
elements. Additional parameters are:

=over 4 

=item C<values>

A list of values from where to choose randomly.

=item C<quantity> (optional. default: 1)

How many items each call will produce, returned as a list.

=back

Example:

    {   id        => 'Random List Pick Example',
        weight    => 35,
        sql       => 'SELECT * FROM some_table WHERE a_column = ?',
        arguments => {
            type       => 'random_from_list',
            values => [ 'A', 'B', 'C', 'D', 'E' ]
        },
    }

=head1 METHODS

=head2 new 

Constructor.

=cut 

sub new {
    my ( $class, $query_descriptor ) = @_;
    my $self = bless {}, $class;
    $self->__process_query_descriptor($query_descriptor);
    return $self;
}

{
    my %function = (
        constant => sub {
            my $arguments = shift;
            my ( $value, $quantity ) = @{$arguments}{qw( value quantity )};
            $quantity ||= 1;
            return sub {
                my @list;
                push @list, $value while $quantity-- > 0;
                return @list;
                }
        },
        list => sub {
            my $arguments = shift;
            my $values    = $$arguments{values};
            return sub { return @$values };
        },
        random_integer => sub {
            my $arguments = shift;
            my ( $min, $max, $quantity )
                = @{$arguments}{qw(min max quantity)};
            $quantity ||= 1;
            return sub {
                my @list;
                push @list, int( rand($max) ) + $min while $quantity-- > 0;
                return @list;
            };
        },
        random_from_list => sub {
            my $arguments = shift;
            my ( $list, $quantity ) = @{$arguments}{qw(list quantity)};
            $quantity ||= 1;
            return sub {
                my @list;
                push @list, $$list[ int( rand( scalar(@$list) ) ) ]
                    while $quantity-- > 0;
                return @list;
            };
        },
    );

    sub __process_query_descriptor {
        my ( $self, $query_descriptor ) = @_;

        die 'No query ID?!' unless exists $$query_descriptor{id};
        $$self{id} = $$query_descriptor{id};

        $$self{weight} = $$query_descriptor{weight};
        $$self{weight} ||= 1; # Default

        die 'No SQL?!' unless exists $$query_descriptor{sql};
        $$self{sql} = $$query_descriptor{sql};

        die 'No arguments?!' unless exists $$query_descriptor{arguments};
        $$self{__parameters} = [
            map {
                die qq(There's no generation function for type '$$_{type}'.)
                    unless exists $function{ $$_{type} };
                $function{ $$_{type} }->($_)
            } @{ $$query_descriptor{arguments} }
        ];
    }
}

=head2 id

Accessor. Returns the ID for this query.

=cut 

sub id { ${ $_[0] }{id} }

=head2 weight

Accessor. Returns the weight associated with this query.

If a second value is provided, it will be stored as this instance's weight.

=cut 

sub weight { defined $_[1]? ${ $_[0] }{weight} = $_[1] : ${ $_[0] }{weight} }

=head2 sql

Accessor. Returns the Parametrised SQL Code represented by this query object,
suitable for the L<DBI::prepare> method call.

=cut

sub sql { ${ $_[0] }{sql} }

=head2 parameters

Returns a list of parameters suitable to be fed to L<DBI::execute>, containing
as many arguments as specified and in the same order as the specification in
the C<arguments> input parameter.

=cut 

sub parameters {
    my ($self) = @_;
    return map { $_->() } @{ $$self{__parameters} };
}

=head1 AUTHOR

Luis Motta Campos, C<< <lucampos at ebay.com> >>

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2013-2014 eBay Software Foundation

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

=cut


1;    # End of MySQL::Benchmark::Query

