The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Search::Query::Dialect::DBIxClass;
$Search::Query::Dialect::DBIxClass::VERSION = '0.005';
# ABSTRACT: Search::Query dialect for simple DBIx::Class query generation
use Moo;
extends 'Search::Query::Dialect::Native';

my %negated_prefix = (
    '+' => '-',
    '-' => '+',
);

my %negated_op = (
    ':'  => '!~',
    '~'  => '!~',
    '='  => '!=',
    '==' => '!=',
    '>'  => '<=',
    '<'  => '>=',
    '#'  => '!#',
    '()' => '()',
);

# add the negated negations
# we don't care if a hash key gets overwritten because they are just different
# chars for the same op
%negated_op = ( %negated_op, reverse %negated_op );


sub BUILD {
    my $self = shift;

    my $op_regex = $self->parser->{op_regex};
    $self->parser->{op_regex} = qr/$op_regex|!#/;

    return $self;
}

sub _wrap_dbic_ops {
    return scalar @_ == 1
        ? $_[0]
        : [@_];
}

sub _dbic_op {
    my ( $self, $clause, $prefix, $colnames ) = @_;
    die 'no clause'
        unless defined $clause;
    die 'no prefix'
        unless defined $prefix;
    die 'column names required'
        unless defined $colnames;

    # normalize operator
    my $op = $clause->{op} || ":";

    # the - prefix inverts the ops
    if ( $prefix eq '-' ) {
        $op =
            exists $negated_op{$op}
            ? $negated_op{$op}
            : die "negated op for '$op' not handled by DBIxClass dialect";
    }

    if ( $op eq '()' ) {
        return $self->as_dbic_query( $clause->{value}, $prefix );
    }
    elsif ( $op eq ':' || $op eq '~' ) {
        return _wrap_dbic_ops(
            map {
                \[  "LOWER($_) LIKE ?",
                    [ plain_value => "%$clause->{value}%" ]
                    ]
            } @$colnames
        );
    }
    elsif ( $op eq '!~' ) {
        return _wrap_dbic_ops(
            map {
                \[  "COALESCE( LOWER($_), '' ) NOT LIKE ?",
                    [ plain_value => "%$clause->{value}%" ]
                    ]
            } @$colnames
        );
    }
    elsif ( $op eq '=' || $op eq '==' ) {
        return _wrap_dbic_ops(
            map {
                { $_ => $clause->{value} }
            } @$colnames
        );
    }
    elsif ( $op eq '>' || $op eq '<' || $op eq '>=' || $op eq '<=' ) {
        return _wrap_dbic_ops(
            map {
                { $_ => { $op => $clause->{value} } }
            } @$colnames
        );
    }
    elsif ( $op eq '#' ) {
        return _wrap_dbic_ops(
            map {
                { $_ => { -in => [ split( /,/, $clause->{value} ) ] } }
            } @$colnames
        );
    }
    elsif ( $op eq '!#' ) {
        return _wrap_dbic_ops(
            map {
                { $_ => { -not_in => [ split( /,/, $clause->{value} ) ] } }
            } @$colnames
        );
    }
    else {
        die "operator '$op' not supported by DBIxClass dialect";
    }
}


sub as_dbic_query {
    my $self         = shift;
    my $tree         = shift || $self;    # if called recursively by the () op
    my $query_prefix = shift || '+';

    # ensure default_field is always an arrayref
    my @default_fields =
        ( exists $self->parser->{default_field}
            && !ref $self->parser->{default_field} )
        ? [ $self->parser->{default_field} ]
        : @{ $self->parser->{default_field} };

    my @q;
    foreach my $prefix ( '+', '-' ) {
        next unless exists $tree->{$prefix};

        for my $clause ( @{ $tree->{$prefix} } ) {
            my @colnames =
                defined $clause->{field}
                ? ( $clause->{field} )
                : @default_fields;

            my $clause_prefix =
                  $query_prefix eq '-'
                ? $negated_prefix{$prefix}
                : $prefix;

            my $dbic_op =
                $self->_dbic_op( $clause, $clause_prefix, \@colnames );

            push @q, $dbic_op;
        }
    }

    my %search_params = ( -and => \@q );

    return \%search_params;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Search::Query::Dialect::DBIxClass - Search::Query dialect for simple DBIx::Class query generation

=head1 VERSION

version 0.005

=head1 SYNOPSIS

    use Test::DBIx::Class::Example::Schema;
    use Search::Query;

    my $schema = Test::DBIx::Class::Example::Schema->connect();
    my $query = Search::Query->parser(
            dialect => 'DBIxClass',
            default_field => [qw( name description )],
        )->parse('foo bar -baz');
    my $rs = $schema->resultset('Foo')->search($query->as_dbic_query);

=head1 DESCRIPTION

Search::Query::Dialect::DBIxClass extends L<Search::Query::Dialect::Native>
by an as_dbic_query method that returns a hashref that can be passed to
L<DBIx::Class::ResultSet/search>.

=head1 METHODS

=head2 BUILD

Overrides base method and sets DBIx::Class-appropriate defaults.
It adds '!#' to the op_regex which is the 'not in list of values' operator.

=head2 as_dbic_query

    Returns the query as hashref that can be passed to
    L<DBIx::Class::ResultSet/search>.

=head1 AUTHOR

Alexander Hartmaier <abraxxa@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Alexander Hartmaier.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut