package SQL::Maker::Condition; use strict; use warnings; use utf8; use Scalar::Util (); use SQL::Maker::Util; use overload '&' => sub { $_[0]->compose_and($_[1]) }, '|' => sub { $_[0]->compose_or($_[1]) }, fallback => 1; sub _quote { my ($self, $label) = @_; return $$label if ref $label; SQL::Maker::Util::quote_identifier($label, $self->{quote_char}, $self->{name_sep}) } sub new { my $class = shift; my %args = @_==1 ? %{$_[0]} : @_; bless {sql => [], bind => [], strict => 0, %args}, $class; } sub _make_term { my ($self, $col, $val) = @_; if (Scalar::Util::blessed($val)) { if ($val->can('as_sql')) { return ($val->as_sql($col, sub { $self->_quote(@_) }), [ $val->bind() ]); } else { return ($self->_quote($col) . " = ?", [ $val ]); } } Carp::croak("cannot pass in an unblessed ref as an argument in strict mode") if ref($val) && $self->{strict}; if ( ref($val) eq 'ARRAY' ) { # make_term(foo => {-and => [1,2,3]}) => (foo = 1) AND (foo = 2) AND (foo = 3) if ( ref $val->[0] or ( ( $val->[0] || '' ) eq '-and' ) ) { my $logic = 'OR'; my @values = @$val; if ( $val->[0] eq '-and' ) { $logic = 'AND'; shift @values; } my @bind; my @terms; for my $v (@values) { my ( $term, $bind ) = $self->_make_term( $col, $v ); push @terms, "($term)"; push @bind, @$bind; } my $term = join " $logic ", @terms; return ($term, \@bind); } else { # make_term(foo => [1,2,3]) => foo IN (1,2,3) return $self->_make_term_by_arrayref($col, 'IN', $val); } } elsif ( ref($val) eq 'HASH' ) { my ( $op, $v ) = ( %{$val} ); $op = uc($op); if ( ( $op eq 'IN' || $op eq 'NOT IN' ) && ref($v) eq 'ARRAY' ) { return $self->_make_term_by_arrayref($col, $op, $v); } elsif ( ( $op eq 'IN' || $op eq 'NOT IN' ) && ref($v) eq 'REF' ) { # make_term(foo => +{ 'IN', \['SELECT foo FROM bar'] }) => foo IN (SELECT foo FROM bar) my @values = @{$$v}; my $term = $self->_quote($col) . " $op (" . shift(@values) . ')'; return ($term, \@values); } elsif ( ( $op eq 'BETWEEN' ) && ref($v) eq 'ARRAY' ) { Carp::croak("USAGE: make_term(foo => {BETWEEN => [\$a, \$b]})") if @$v != 2; return ($self->_quote($col) . " BETWEEN ? AND ?", $v); } else { if (ref($v) eq 'SCALAR') { # make_term(foo => +{ '<', \"DATE_SUB(NOW(), INTERVAL 3 DAY)"}) => 'foo < DATE_SUB(NOW(), INTERVAL 3 DAY)' return ($self->_quote($col) . " $op " . $$v, []); } else { # make_term(foo => +{ '<', 3 }) => foo < 3 return ($self->_quote($col) . " $op ?", [$v]); } } } elsif ( ref($val) eq 'SCALAR' ) { # make_term(foo => \"> 3") => foo > 3 return ($self->_quote($col) . " $$val", []); } elsif ( ref($val) eq 'REF') { my ($query, @v) = @{${$val}}; return ($self->_quote($col) . " $query", \@v); } else { if (defined $val) { # make_term(foo => "3") => foo = 3 return ($self->_quote($col) . " = ?", [$val]); } else { # make_term(foo => undef) => foo IS NULL return ($self->_quote($col) . " IS NULL", []); } } } sub _make_term_by_arrayref { my ($self, $col, $op, $v) = @_; if (@$v == 0) { if ($op eq 'IN') { # make_term(foo => +{'IN' => []}) => 0=1 return ('0=1', []); } else { # make_term(foo => +{'NOT IN' => []}) => 1=1 return ('1=1', []); } } else { # make_term(foo => +{ 'IN', [1,2,3] }) => foo IN (1,2,3) my $term = $self->_quote($col) . " $op (" . '?, ' x (scalar @$v - 1) . '?)'; return ($term, $v); } } sub add { my ( $self, $col, $val ) = @_; my ( $term, $bind ) = $self->_make_term( $col, $val ); push @{ $self->{sql} }, "($term)"; push @{ $self->{bind} }, @$bind; return $self; # for influent interface } sub add_raw { my ($self, $term, $bind) = @_; push @{ $self->{sql} }, "($term)"; if ( defined $bind ) { push @{ $self->{bind} }, (ref($bind) eq 'ARRAY' ? @$bind : $bind); } return $self; } sub compose_and { my ($self, $other) = @_; if ( !@{$self->{sql}} ) { if ( !@{$other->{sql}} ) { return SQL::Maker::Condition->new; } return SQL::Maker::Condition->new( sql => ['(' . $other->as_sql() . ')'], bind => [@{$other->{bind}}], ); } if ( !@{$other->{sql}} ) { return SQL::Maker::Condition->new( sql => ['(' . $self->as_sql() . ')'], bind => [@{$self->{bind}}], ); } return SQL::Maker::Condition->new( sql => ['(' . $self->as_sql() . ') AND (' . $other->as_sql() . ')'], bind => [@{$self->{bind}}, @{$other->{bind}}], ); } sub compose_or { my ($self, $other) = @_; if ( !@{$self->{sql}} ) { if ( !@{$other->{sql}} ) { return SQL::Maker::Condition->new; } return SQL::Maker::Condition->new( sql => ['(' . $other->as_sql() . ')'], bind => [@{$other->{bind}}], ); } if ( !@{$other->{sql}} ) { return SQL::Maker::Condition->new( sql => ['(' . $self->as_sql() . ')'], bind => [@{$self->{bind}}], ); } # return value is enclosed with '()'. # because 'OR' operator priority less than 'AND'. return SQL::Maker::Condition->new( sql => ['((' . $self->as_sql() . ') OR (' . $other->as_sql() . '))'], bind => [@{$self->{bind}}, @{$other->{bind}}], ); } sub as_sql { my ($self) = @_; return join(' AND ', @{$self->{sql}}); } sub bind { my $self = shift; return wantarray ? @{$self->{bind}} : $self->{bind}; } 1; __END__ =for test_synopsis my ($sql, @bind); =head1 NAME SQL::Maker::Condition - condition object for SQL::Maker =head1 SYNOPSIS my $condition = SQL::Maker::Condition->new( name_sep => '.', quote_char => '`', ); $condition->add('foo_id' => 3); $condition->add('bar_id' => 4); $sql = $condition->as_sql(); # (`foo_id`=?) AND (`bar_id`=?) @bind = $condition->bind(); # (3, 4) # add_raw my $condition = SQL::Maker::Condition->new( name_sep => '.', quote_char => '`', ); $condition->add_raw('EXISTS(SELECT * FROM bar WHERE name = ?)' => ['john']); $condition->add_raw('type IS NOT NULL'); $sql = $condition->as_sql(); # (EXISTS(SELECT * FROM bar WHERE name = ?)) AND (type IS NOT NULL) @bind = $condition->bind(); # ('john') # composite and my $other = SQL::Maker::Condition->new( name_sep => '.', quote_char => '`', ); $other->add('name' => 'john'); my $comp_and = $condition & $other; $sql = $comp_and->as_sql(); # ((`foo_id`=?) AND (`bar_id`=?)) AND (`name`=?) @bind = $comp_and->bind(); # (3, 4, 'john') # composite or my $comp_or = $condition | $other; $sql = $comp_and->as_sql(); # ((`foo_id`=?) AND (`bar_id`=?)) OR (`name`=?) @bind = $comp_and->bind(); # (3, 4, 'john') =head1 CONDITION CHEAT SHEET Here is a cheat sheet for conditions. IN: ['foo','bar'] OUT QUERY: '`foo` = ?' OUT BIND: ('bar') IN: ['foo',['bar','baz']] OUT QUERY: '`foo` IN (?, ?)' OUT BIND: ('bar','baz') IN: ['foo',{'IN' => ['bar','baz']}] OUT QUERY: '`foo` IN (?, ?)' OUT BIND: ('bar','baz') IN: ['foo',{'not IN' => ['bar','baz']}] OUT QUERY: '`foo` NOT IN (?, ?)' OUT BIND: ('bar','baz') IN: ['foo',{'!=' => 'bar'}] OUT QUERY: '`foo` != ?' OUT BIND: ('bar') IN: ['foo',\'IS NOT NULL'] OUT QUERY: '`foo` IS NOT NULL' OUT BIND: () IN: ['foo',{'between' => ['1','2']}] OUT QUERY: '`foo` BETWEEN ? AND ?' OUT BIND: ('1','2') IN: ['foo',{'like' => 'xaic%'}] OUT QUERY: '`foo` LIKE ?' OUT BIND: ('xaic%') IN: ['foo',[{'>' => 'bar'},{'<' => 'baz'}]] OUT QUERY: '(`foo` > ?) OR (`foo` < ?)' OUT BIND: ('bar','baz') IN: ['foo',['-and',{'>' => 'bar'},{'<' => 'baz'}]] OUT QUERY: '(`foo` > ?) AND (`foo` < ?)' OUT BIND: ('bar','baz') IN: ['foo',['-and','foo','bar','baz']] OUT QUERY: '(`foo` = ?) AND (`foo` = ?) AND (`foo` = ?)' OUT BIND: ('foo','bar','baz') IN: ['foo_id',\['IN (SELECT foo_id FROM bar WHERE t=?)',44]] OUT QUERY: '`foo_id` IN (SELECT foo_id FROM bar WHERE t=?)' OUT BIND: ('44') IN: ['foo_id', {IN => \['SELECT foo_id FROM bar WHERE t=?',44]}] OUT QUERY: '`foo_id` IN (SELECT foo_id FROM bar WHERE t=?)' OUT BIND: ('44') IN: ['foo_id',\['MATCH (col1, col2) AGAINST (?)','apples']] OUT QUERY: '`foo_id` MATCH (col1, col2) AGAINST (?)' OUT BIND: ('apples') IN: ['foo_id',undef] OUT QUERY: '`foo_id` IS NULL' OUT BIND: () IN: ['foo_id',{'IN' => []}] OUT QUERY: '0=1' OUT BIND: () IN: ['foo_id',{'NOT IN' => []}] OUT QUERY: '1=1' OUT BIND: () IN: ['foo_id', [123,sql_type(\3, SQL_INTEGER)]] OUT QUERY: '`foo_id` IN (?, ?)' OUT BIND: (123, sql_type(\3, SQL_INTEGER)) IN: ['foo_id', sql_type(\3, SQL_INTEGER)] OUT QUERY: '`foo_id` = ?' OUT BIND: sql_type(\3, SQL_INTEGER) IN: ['created_on', { '>', \'DATE_SUB(NOW(), INTERVAL 1 DAY)' }] OUT QUERY: '`created_on` > DATE_SUB(NOW(), INTERVAL 1 DAY)' OUT BIND: It is also possible to use the functions exported by C to define the conditions. IN: ['foo' => sql_in(['bar','baz'])] OUT QUERY: '`foo` IN (?,?)' OUT BIND: ('bar','baz') IN: ['foo' => sql_lt(3)] OUT QUERY: '`foo` < ?' OUT BIND: (3) IN: ['foo' => sql_not_in(['bar','baz'])] OUT QUERY: '`foo` NOT IN (?,?)' OUT BIND: ('bar','baz') IN: ['foo' => sql_ne('bar')] OUT QUERY: '`foo` != ?' OUT BIND: ('bar') IN: ['foo' => sql_is_not_null()] OUT QUERY: '`foo` IS NOT NULL' OUT BIND: () IN: ['foo' => sql_between('1','2')] OUT QUERY: '`foo` BETWEEN ? AND ?' OUT BIND: ('1','2') IN: ['foo' => sql_like('xaic%')] OUT QUERY: '`foo` LIKE ?' OUT BIND: ('xaic%') IN: ['foo' => sql_or([sql_gt('bar'), sql_lt('baz')])] OUT QUERY: '(`foo` > ?) OR (`foo` < ?)' OUT BIND: ('bar','baz') IN: ['foo' => sql_and([sql_gt('bar'), sql_lt('baz')])] OUT QUERY: '(`foo` > ?) AND (`foo` < ?)' OUT BIND: ('bar','baz') IN: ['foo_id' => sql_op('IN (SELECT foo_id FROM bar WHERE t=?)',[44])] OUT QUERY: '`foo_id` IN (SELECT foo_id FROM bar WHERE t=?)' OUT BIND: ('44') IN: ['foo_id' => sql_in([sql_raw('SELECT foo_id FROM bar WHERE t=?',44)])] OUT QUERY: '`foo_id` IN ((SELECT foo_id FROM bar WHERE t=?))' OUT BIND: ('44') IN: ['foo_id', => sql_op('MATCH (@) AGAINST (?)',['apples'])] OUT QUERY: 'MATCH (`foo_id`) AGAINST (?)' OUT BIND: ('apples') IN: ['foo_id',undef] OUT QUERY: '`foo_id` IS NULL' OUT BIND: () IN: ['foo_id',sql_in([])] OUT QUERY: '0=1' OUT BIND: () IN: ['foo_id',sql_not_in([])] OUT QUERY: '1=1' OUT BIND: () IN: ['foo_id', sql_type(\3, SQL_INTEGER)] OUT QUERY: '`foo_id` = ?' OUT BIND: sql_type(\3, SQL_INTEGER) IN: ['foo_id', sql_in([sql_type(\3, SQL_INTEGER)])] OUT QUERY: '`foo_id` IN (?)' OUT BIND: sql_type(\3, SQL_INTEGER) IN: ['created_on', sql_gt(sql_raw('DATE_SUB(NOW(), INTERVAL 1 DAY)')) ] OUT QUERY: '`created_on` > DATE_SUB(NOW(), INTERVAL 1 DAY)' OUT BIND: =head1 SEE ALSO L