diff --git a/cpanfile b/cpanfile index 14019ac..f4639d0 100644 --- a/cpanfile +++ b/cpanfile @@ -22,6 +22,7 @@ on develop => sub { on test => sub { requires 'version'; + requires 'Tie::IxHash'; }; feature 'test_sqlite', 'Test SQLite' => sub { diff --git a/lib/Data/ObjectDriver/Driver/DBI.pm b/lib/Data/ObjectDriver/Driver/DBI.pm index 148db0d..449f6eb 100644 --- a/lib/Data/ObjectDriver/Driver/DBI.pm +++ b/lib/Data/ObjectDriver/Driver/DBI.pm @@ -12,6 +12,7 @@ use Data::ObjectDriver::Errors; use Data::ObjectDriver::SQL; use Data::ObjectDriver::Driver::DBD; use Data::ObjectDriver::Iterator; +use Scalar::Util 'blessed'; my $ForkSafe = _is_fork_safe(); my %Handles; @@ -172,19 +173,25 @@ sub prepare_fetch { sub fetch { my $driver = shift; - my($rec, $class, $orig_terms, $orig_args) = @_; + my ($rec, $class, $terms_or_stmt, $orig_args) = @_; + my ($sql, $stmt); if ($Data::ObjectDriver::RESTRICT_IO) { use Data::Dumper; - die "Attempted DBI I/O while in restricted mode: fetch() " . Dumper($orig_terms, $orig_args); + die "Attempted DBI I/O while in restricted mode: fetch() " . Dumper($terms_or_stmt, $orig_args); } - my ($sql, $bind, $stmt) = $driver->prepare_fetch($class, $orig_terms, $orig_args); + if (blessed($terms_or_stmt) && $terms_or_stmt->isa('Data::ObjectDriver::SQL')) { + $sql = $terms_or_stmt->as_sql; + $stmt = $terms_or_stmt; + } else { + ($sql, undef, $stmt) = $driver->prepare_fetch($class, $terms_or_stmt, $orig_args); + } - my @bind; + my @columns; my $map = $stmt->select_map; for my $col (@{ $stmt->select }) { - push @bind, \$rec->{ $map->{$col} }; + push @columns, \$rec->{ $map->{$col} }; } my $dbh = $driver->r_handle($class->properties->{db}); @@ -192,7 +199,7 @@ sub fetch { my $sth = $orig_args->{no_cached_prepare} ? $dbh->prepare($sql) : $driver->_prepare_cached($dbh, $sql); $sth->execute(@{ $stmt->{bind} }); - $sth->bind_columns(undef, @bind); + $sth->bind_columns(undef, @columns); # need to slurp 'offset' rows for DBs that cannot do it themselves if (!$driver->dbd->offset_implemented && $orig_args->{offset}) { @@ -218,11 +225,11 @@ sub load_object_from_rec { } sub search { - my($driver) = shift; - my($class, $terms, $args) = @_; + my ($driver) = shift; + my ($class, $terms_or_stmt, $args) = @_; my $rec = {}; - my $sth = $driver->fetch($rec, $class, $terms, $args); + my $sth = $driver->fetch($rec, $class, $terms_or_stmt, $args); my $iter = sub { ## This is kind of a hack--we need $driver to stay in scope, diff --git a/lib/Data/ObjectDriver/SQL.pm b/lib/Data/ObjectDriver/SQL.pm index 8c43856..06f3a9f 100644 --- a/lib/Data/ObjectDriver/SQL.pm +++ b/lib/Data/ObjectDriver/SQL.pm @@ -3,6 +3,7 @@ package Data::ObjectDriver::SQL; use strict; use warnings; +use Scalar::Util 'blessed'; use base qw( Class::Accessor::Fast ); @@ -10,7 +11,7 @@ __PACKAGE__->mk_accessors(qw( select distinct select_map select_map_reverse from joins where bind limit offset group order having where_values column_mutator index_hint - comment + comment as )); sub new { @@ -33,10 +34,16 @@ sub new { sub add_select { my $stmt = shift; my($term, $col) = @_; - $col ||= $term; push @{ $stmt->select }, $term; - $stmt->select_map->{$term} = $col; - $stmt->select_map_reverse->{$col} = $term; + if (blessed($term) && $term->isa('Data::ObjectDriver::SQL')) { + my $alias = $col || $term->as || $term->as_sql; + $stmt->select_map->{$term} = $alias; + $stmt->select_map_reverse->{$alias} = $term; + } else { + $col ||= $term; + $stmt->select_map->{$term} = $col; + $stmt->select_map_reverse->{$col} = $term; + } } sub add_join { @@ -60,12 +67,26 @@ sub add_index_hint { sub as_sql { my $stmt = shift; my $sql = ''; + my @bind_for_select; + if (@{ $stmt->select }) { $sql .= 'SELECT '; $sql .= 'DISTINCT ' if $stmt->distinct; + my $select_map = $stmt->select_map; $sql .= join(', ', map { - my $alias = $stmt->select_map->{$_}; - $alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias"; + my $col = $_; + my $alias = $select_map->{$col}; + if (blessed($col) && $col->isa('Data::ObjectDriver::SQL')) { + push @bind_for_select, @{ $col->{bind} }; + @{ $col->{bind} } = (); + $col->as_subquery($alias); + } else { + if ($alias) { + /(?:^|\.)\Q$alias\E$/ ? $col : "$col $alias"; + } else { + $col; + } + } } @{ $stmt->select }) . "\n"; } $sql .= 'FROM '; @@ -91,8 +112,19 @@ sub as_sql { $sql .= ', ' if @from; } + my @bind_for_from; + if (@from) { - $sql .= join ', ', map { $stmt->_add_index_hint($_) } @from; + $sql .= join ', ', map { + my $from = $_; + if (blessed($from) && $from->isa('Data::ObjectDriver::SQL')) { + push @bind_for_from, @{$from->{bind}}; + @{$from->{bind}} = (); + $from->as_subquery; + } else { + $stmt->_add_index_hint($from); + } + } @from; } $sql .= "\n"; @@ -107,9 +139,22 @@ sub as_sql { if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) { $sql .= "-- $1" if $1; } + + @{ $stmt->{bind} } = (@bind_for_select, @bind_for_from, @{ $stmt->{bind} }); + return $sql; } +sub as_subquery { + my ($stmt, $alias) = @_; + my $subquery = '(' . $stmt->as_sql . ')'; + $alias ||= $stmt->as; + if ($alias) { + $subquery .= ' AS ' . $alias; + } + $subquery; +} + sub as_limit { my $stmt = shift; my $n = $stmt->limit or @@ -231,7 +276,11 @@ sub add_having { # Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/; if (my $orig = $stmt->select_map_reverse->{$col}) { - $col = $orig; + if (blessed($orig) && $orig->isa('Data::ObjectDriver::SQL')) { + # do nothins + } else { + $col = $orig; + } } my($term, $bind) = $stmt->_mk_term($col, $val); @@ -281,12 +330,17 @@ sub _mk_term { $term = "$c $op ? AND ?"; push @bind, @{$val->{value}}; } else { - if (ref $val->{value} eq 'SCALAR') { - $term = "$c $val->{op} " . ${$val->{value}}; + my $value = $val->{value}; + if (ref $value eq 'SCALAR') { + $term = "$c $val->{op} " . $$value; + } elsif (blessed($value) && $value->isa('Data::ObjectDriver::SQL')) { + local $value->{as} = undef; + $term = "$c $val->{op} ". $value->as_subquery; + push @bind, @{$value->{bind}}; } else { $term = "$c $val->{op} ?"; $term .= $stmt->as_escape($val->{escape}) if $val->{escape} && $op =~ /^(?:NOT\s+)?I?LIKE$/; - push @bind, $val->{value}; + push @bind, $value; } } } elsif (ref($val) eq 'SCALAR') { diff --git a/t/11-sql-with-models.t b/t/11-sql-with-models.t new file mode 100644 index 0000000..4c95276 --- /dev/null +++ b/t/11-sql-with-models.t @@ -0,0 +1,390 @@ +# $Id$ + +use strict; +use warnings; + +use lib 't/lib'; +use lib 't/lib/sql'; +use Test::More; +use DodTestUtil; +use Tie::IxHash; + +BEGIN { DodTestUtil->check_driver } + +use Blog; +use Entry; + +sub ordered_hashref { + tie my %params, Tie::IxHash::, @_; + return \%params; +} + +setup_dbs({ + global => [qw( blog entry )], +}); + +my $blog1 = Blog->new(name => 'blog1'); +$blog1->save; +my $blog2 = Blog->new(parent_id => $blog1->id, name => 'blog2'); +$blog2->save; +my $entry11 = Entry->new(blog_id => $blog1->id, title => 'title11', text => 'first'); +$entry11->save; +my $entry12 = Entry->new(blog_id => $blog1->id, title => 'title12', text => 'second'); +$entry12->save; +my $entry21 = Entry->new(blog_id => $blog2->id, title => 'title21', text => 'first'); +$entry21->save; +my $entry22 = Entry->new(blog_id => $blog2->id, title => 'title22', text => 'second'); +$entry22->save; + +subtest 'as_subquery' => sub { + my $stmt = Blog->driver->prepare_statement('Blog', { name => 'foo' }, { fetchonly => ['id'] }); + + is(sql_normalize($stmt->as_subquery), sql_normalize(<<'EOF'), 'right sql'); +(SELECT blog.id FROM blog WHERE (blog.name = ?)) +EOF + is_deeply($stmt->{bind}, ['foo'], 'right bind values'); + + $stmt->as('mysubquery'); + + is(sql_normalize($stmt->as_subquery), sql_normalize(<<'EOF'), 'right sql'); +(SELECT blog.id FROM blog WHERE (blog.name = ?)) AS mysubquery +EOF +}; + +subtest 'do not aggregate bind twice' => sub { + + my $stmt = Blog->driver->prepare_statement('Blog', { name => $blog1->name }, {}); + my $subquery = Entry->driver->prepare_statement( + 'Entry', + ordered_hashref(blog_id => \'= blog.id', text => 'second'), + { fetchonly => ['id'], limit => 1 }); + $subquery->as('sub'); + $stmt->add_select($subquery); + $stmt->as_sql; + is scalar(@{ $stmt->bind }), 2; + $stmt->as_sql; + is scalar(@{ $stmt->bind }), 2; +}; + +subtest 'subquery in select clause' => sub { + + subtest 'fetch blogs and include a entry with specific text if any' => sub { + my $stmt = Blog->driver->prepare_statement('Blog', { name => $blog1->name }, {}); + my $subquery = Entry->driver->prepare_statement( + 'Entry', + ordered_hashref(blog_id => \'= blog.id', text => 'second'), + { fetchonly => ['id'], limit => 1 }); + $subquery->as('sub_alias'); + $stmt->add_select($subquery); + + my $expected = sql_normalize(<<'EOF'); +SELECT + blog.id, + blog.parent_id, + blog.name, + ( + SELECT entry.id + FROM entry + WHERE (entry.blog_id = blog.id) AND (entry.text = ?) + LIMIT 1 + ) AS sub_alias +FROM blog +WHERE (blog.name = ?) +EOF + + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['second', $blog1->name], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 1; + is scalar(keys %{ $res[0]{column_values} }), 4; + is($res[0]{column_values}{id}, $blog1->id); + is($res[0]{column_values}{sub_alias}, $entry12->id); + }; + + subtest 'set alias by add_select argument' => sub { + my $stmt = Blog->driver->prepare_statement('Blog', { name => $blog1->name }, {}); + my $subquery = Entry->driver->prepare_statement( + 'Entry', + ordered_hashref(blog_id => \'= blog.id', text => 'second'), + { fetchonly => ['id'], limit => 1 }); + $stmt->add_select($subquery, 'sub_alias'); + + my $expected = sql_normalize(<<'EOF'); +SELECT + blog.id, + blog.parent_id, + blog.name, + ( + SELECT entry.id + FROM entry + WHERE (entry.blog_id = blog.id) AND (entry.text = ?) + LIMIT 1 + ) AS sub_alias +FROM blog +WHERE (blog.name = ?) +EOF + + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['second', $blog1->name], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 1; + is scalar(keys %{ $res[0]{column_values} }), 4; + is($res[0]{column_values}{id}, $blog1->id); + is($res[0]{column_values}{sub_alias}, $entry12->id); + }; +}; + +subtest 'select_map used in add_having' => sub { + my $stmt = Entry->driver->prepare_statement('Entry', {}, {}); + $stmt->add_select('count(*)', 'count'); + $stmt->group({column => 'blog_id'}); + $stmt->add_having(count => 2); + is sql_normalize($stmt->as_sql), sql_normalize(<<'EOF'); +SELECT entry.id, entry.blog_id, entry.title, entry.text, count(*) count +FROM entry +GROUP BY blog_id +HAVING (count(*) = ?) +EOF + is_deeply($stmt->{bind}, ['2'], 'right bind values'); + + my $subquery = Blog->driver->prepare_statement('Blog', {}, {}); + $stmt->add_select($subquery, 'sub'); + $stmt->add_having(sub => 3); + is sql_normalize($stmt->as_sql), sql_normalize(<<'EOF'); +SELECT + entry.id, entry.blog_id, entry.title, entry.text, count(*) count, + (SELECT blog.id, blog.parent_id, blog.name FROM blog) AS sub +FROM entry +GROUP BY blog_id +HAVING (count(*) = ?) AND (sub = ?) +EOF + is_deeply($stmt->{bind}, ['2', '3'], 'right bind values'); +}; + +subtest 'subquery in from clause' => sub { + + subtest 'blogs that has entries with specific text' => sub { + my $subquery = Entry->driver->prepare_statement( + 'Entry', + { text => 'second' }, { fetchonly => ['id', 'blog_id', 'text'] }); + $subquery->as('sub'); + my $stmt = Blog->driver->prepare_statement( + 'Blog', [ + { 'blog.id' => \'= sub.blog_id' }, + { 'blog.id' => [$blog1->id, $blog2->id] }, # FIXME: table prefix should be added automatically (MTC-30879) + ], + {}); + push @{ $stmt->from }, $subquery; + + my $expected = sql_normalize(<<'EOF'); +SELECT + blog.id, + blog.parent_id, + blog.name +FROM blog, + ( + SELECT entry.id, entry.blog_id, entry.text + FROM entry + WHERE (entry.text = ?) + ) AS sub +WHERE ((blog.id = sub.blog_id)) AND ((blog.id IN (?,?))) +EOF + + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['second', $blog1->id, $blog2->id], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 2; + is scalar(keys %{ $res[0]{column_values} }), 3; + is($res[0]{column_values}{id}, $blog1->id); + }; + + subtest 'select list includes sub query result' => sub { + my $subquery = Entry->driver->prepare_statement( + 'Entry', + { text => 'second' }, { fetchonly => ['id', 'blog_id'] }); + # $subquery->add_select('max(id)', 'max_entry_id'); + $subquery->as('sub'); + my $stmt = Blog->driver->prepare_statement( + 'Blog', [ + { 'blog.id' => \'= sub.blog_id' }, # FIXME: table prefix should be added automatically (MTC-30879) + { 'blog.id' => [$blog1->id, $blog2->id] }, # FIXME: table prefix should be added automatically (MTC-30879) + ], + {}); + push @{ $stmt->from }, $subquery; + $stmt->add_select('sub.id', 'entry_id'); + + my $expected = sql_normalize(<<'EOF'); +SELECT + blog.id, + blog.parent_id, + blog.name, + sub.id entry_id +FROM blog, + ( + SELECT entry.id, entry.blog_id + FROM entry + WHERE (entry.text = ?) + ) AS sub +WHERE ((blog.id = sub.blog_id)) AND ((blog.id IN (?,?))) +EOF + + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['second', $blog1->id, $blog2->id], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 2; + is scalar(keys %{ $res[0]{column_values} }), 4; + is($res[0]{column_values}{entry_id}, $entry12->id); + is($res[1]{column_values}{entry_id}, $entry22->id); + }; +}; + +subtest 'subquery in where clause' => sub { + + subtest 'entries that belongs to subquery blogs' => sub { + my $stmt = Entry->driver->prepare_statement( + 'Entry', + ordered_hashref( + text => 'first', + blog_id => { + op => 'IN', + value => Blog->driver->prepare_statement( + 'Blog', + { name => { op => 'LIKE', value => 'blog1', escape => '!' } }, + { fetchonly => ['id'] } + ), + } + ), + { limit => 4 }); + + my $expected = sql_normalize(<<'EOF'); +SELECT + entry.id, entry.blog_id, entry.title, entry.text +FROM + entry +WHERE + (entry.text = ?) + AND + (entry.blog_id IN (SELECT blog.id FROM blog WHERE (blog.name LIKE ? ESCAPE '!'))) +LIMIT 4 +EOF + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['first', 'blog1'], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 1; + is scalar(keys %{ $res[0]{column_values} }), 4; + is($res[0]{column_values}{id}, $blog1->id); + }; + + subtest 'subquery surrounded by other placeholders' => sub { + my $stmt = Entry->driver->prepare_statement( + 'Entry', + [[ + { text => 'first' }, + '-or', + { + blog_id => { + op => 'IN', + value => Blog->driver->prepare_statement( + 'Blog', [ + { name => { op => 'LIKE', value => 'blog!%', escape => '!' } }, + { name => { op => 'LIKE', value => '!%2', escape => '!' } }, + ], + { fetchonly => ['id'] }) } + }, + '-or', + { text => 'second' }, + ], + { id => [$entry11->id, $entry12->id] }, + ], + { limit => 4 }); + + my $expected = sql_normalize(<<'EOF'); +SELECT + entry.id, entry.blog_id, entry.title, entry.text +FROM + entry +WHERE + ( + ((text = ?)) + OR + ((blog_id IN ( + SELECT blog.id + FROM blog + WHERE ((name LIKE ? ESCAPE '!')) AND ((name LIKE ? ESCAPE '!')) + ))) + OR + ((text = ?)) + ) AND ( + (id IN (?,?)) + ) +LIMIT 4 +EOF + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['first', 'blog!%', '!%2', 'second', $blog1->id, $blog2->id], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 2; + is scalar(keys %{ $res[0]{column_values} }), 4; + is($res[0]{column_values}{id}, $blog1->id); + is($res[1]{column_values}{id}, $blog2->id); + }; +}; + +subtest 'subquery in multiple clauses' => sub { + my $sub1 = Entry->driver->prepare_statement( + 'Entry', + ordered_hashref(blog_id => \'= blog.id', id => { op => '<', value => 99 }), { fetchonly => ['id'] }); + $sub1->select(['max(id)']); + my $sub2 = Entry->driver->prepare_statement('Entry', { text => 'second' }, { fetchonly => ['id'] }); + my $sub3 = Entry->driver->prepare_statement('Entry', { text => 'second' }, { fetchonly => ['blog_id'] }); + $sub1->as('sub1'); + $sub2->as('sub2'); + $sub3->as('sub3'); # this will be ommitted in where clause + my $stmt = Blog->driver->prepare_statement( + 'Blog', { id => { op => 'IN', value => $sub3 } }, + { sort => [{ column => 'blog.id' }, { column => 'sub1' }] }); + $stmt->add_select($sub1); + push @{ $stmt->from }, $sub2; + + my $expected = sql_normalize(<<'EOF'); +SELECT + blog.id, + blog.parent_id, + blog.name, + (SELECT max(id) FROM entry WHERE (entry.blog_id = blog.id) AND (entry.id < ?)) AS sub1 +FROM + blog, + (SELECT entry.id FROM entry WHERE (entry.text = ?)) AS sub2 +WHERE + (blog.id IN (SELECT entry.blog_id FROM entry WHERE (entry.text = ?))) +ORDER BY blog.id ASC, sub1 ASC +EOF + is sql_normalize($stmt->as_sql), sql_normalize($expected), 'right sql'; + is_deeply($stmt->{bind}, ['99', 'second', 'second'], 'right bind values'); + my @res = Blog->driver->search('Blog', $stmt); + is scalar(@res), 4; + is($res[0]{column_values}{id}, $blog1->id); + is($res[0]{column_values}{sub1}, $entry12->id); + is($res[1]{column_values}{id}, $blog1->id); + is($res[1]{column_values}{sub1}, $entry12->id); + is($res[2]{column_values}{id}, $blog2->id); + is($res[2]{column_values}{sub1}, $entry22->id); + is($res[3]{column_values}{id}, $blog2->id); + is($res[3]{column_values}{sub1}, $entry22->id); +}; + +sub sql_normalize { + my $sql = shift; + $sql =~ s{\s+}{ }g; + $sql =~ s{ $}{}g; + $sql =~ s{\( }{(}g; + $sql =~ s{ \)}{)}g; + $sql =~ s{([\(\)]) ([\(\)])}{$1$2}g; + $sql; +} + +END { + disconnect_all(qw/Blog Entry/); + teardown_dbs(qw( global )); +} + +done_testing; diff --git a/t/11-sql.t b/t/11-sql.t index 3742848..b501b73 100644 --- a/t/11-sql.t +++ b/t/11-sql.t @@ -4,305 +4,344 @@ use strict; use warnings; use Data::ObjectDriver::SQL; -use Test::More tests => 112; +use Test::More; my $stmt = ns(); ok($stmt, 'Created SQL object'); -## Testing FROM -$stmt->from([ 'foo' ]); -is($stmt->as_sql, "FROM foo\n"); - -$stmt->from([ 'foo', 'bar' ]); -is($stmt->as_sql, "FROM foo, bar\n"); - -## Testing JOINs -$stmt->from([]); -$stmt->joins([]); -$stmt->add_join(foo => { type => 'inner', table => 'baz', - condition => 'foo.baz_id = baz.baz_id' }); -is($stmt->as_sql, "FROM foo INNER JOIN baz ON foo.baz_id = baz.baz_id\n"); - -$stmt->from([ 'bar' ]); -is($stmt->as_sql, "FROM foo INNER JOIN baz ON foo.baz_id = baz.baz_id, bar\n"); - -$stmt->from([]); -$stmt->joins([]); -$stmt->add_join(foo => [ - { type => 'inner', table => 'baz b1', - condition => 'foo.baz_id = b1.baz_id AND b1.quux_id = 1' }, - { type => 'left', table => 'baz b2', - condition => 'foo.baz_id = b2.baz_id AND b2.quux_id = 2' }, - ]); -is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2\n"; - -# test case for bug found where add_join is called twice -$stmt->joins([]); -$stmt->add_join(foo => [ - { type => 'inner', table => 'baz b1', - condition => 'foo.baz_id = b1.baz_id AND b1.quux_id = 1' }, -]); -$stmt->add_join(foo => [ - { type => 'left', table => 'baz b2', - condition => 'foo.baz_id = b2.baz_id AND b2.quux_id = 2' }, - ]); -is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2\n"; +subtest 'FROM' => sub { + $stmt->from([ 'foo' ]); + is($stmt->as_sql, "FROM foo\n"); + + $stmt->from([ 'foo', 'bar' ]); + is($stmt->as_sql, "FROM foo, bar\n"); +}; -# test case adding another table onto the whole mess -$stmt->add_join(quux => [ - { type => 'inner', table => 'foo f1', - condition => 'f1.quux_id = quux.q_id'} +subtest 'JOINs' => sub { + $stmt->from([]); + $stmt->joins([]); + $stmt->add_join(foo => { type => 'inner', table => 'baz', + condition => 'foo.baz_id = baz.baz_id' }); + is($stmt->as_sql, "FROM foo INNER JOIN baz ON foo.baz_id = baz.baz_id\n"); + + $stmt->from([ 'bar' ]); + is($stmt->as_sql, "FROM foo INNER JOIN baz ON foo.baz_id = baz.baz_id, bar\n"); + + $stmt->from([]); + $stmt->joins([]); + $stmt->add_join(foo => [ + { type => 'inner', table => 'baz b1', + condition => 'foo.baz_id = b1.baz_id AND b1.quux_id = 1' }, + { type => 'left', table => 'baz b2', + condition => 'foo.baz_id = b2.baz_id AND b2.quux_id = 2' }, + ]); + is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2\n"; +}; + +subtest 'bug found where add_join is called twice' => sub { + $stmt->joins([]); + $stmt->add_join(foo => [ + { type => 'inner', table => 'baz b1', + condition => 'foo.baz_id = b1.baz_id AND b1.quux_id = 1' }, ]); + $stmt->add_join(foo => [ + { type => 'left', table => 'baz b2', + condition => 'foo.baz_id = b2.baz_id AND b2.quux_id = 2' }, + ]); + is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2\n"; + + # test case adding another table onto the whole mess + $stmt->add_join(quux => [ + { type => 'inner', table => 'foo f1', + condition => 'f1.quux_id = quux.q_id'} + ]); + + is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2 INNER JOIN foo f1 ON f1.quux_id = quux.q_id\n"; + + # test case for bug found where add_join is called for a table included in the "from". + $stmt->from([ 'foo', 'bar' ]); + $stmt->joins([]); + $stmt->add_join(foo => { type => 'inner', table => 'bar', + condition => 'foo.bar_id = bar.bar_id' }); + is($stmt->as_sql, "FROM foo INNER JOIN bar ON foo.bar_id = bar.bar_id\n"); +}; + +subtest 'GROUP BY' => sub { + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->group({ column => 'baz' }); + is($stmt->as_sql, "FROM foo\nGROUP BY baz\n", 'single bare group by'); + + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->group({ column => 'baz', desc => 'DESC' }); + is($stmt->as_sql, "FROM foo\nGROUP BY baz DESC\n", 'single group by with desc'); + + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->group([ { column => 'baz' }, { column => 'quux' }, ]); + is($stmt->as_sql, "FROM foo\nGROUP BY baz, quux\n", 'multiple group by'); + + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->group([ { column => 'baz', desc => 'DESC' }, + { column => 'quux', desc => 'DESC' }, ]); + is($stmt->as_sql, "FROM foo\nGROUP BY baz DESC, quux DESC\n", 'multiple group by with desc'); +}; + +subtest 'ORDER BY' => sub { + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->order({ column => 'baz', desc => 'DESC' }); + is($stmt->as_sql, "FROM foo\nORDER BY baz DESC\n", 'single order by'); + + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->order([ { column => 'baz', desc => 'DESC' }, + { column => 'quux', desc => 'ASC' }, ]); + is($stmt->as_sql, "FROM foo\nORDER BY baz DESC, quux ASC\n", 'multiple order by'); +}; + +subtest 'GROUP BY plus ORDER BY' => sub { + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->group({ column => 'quux' }); + $stmt->order({ column => 'baz', desc => 'DESC' }); + is($stmt->as_sql, "FROM foo\nGROUP BY quux\nORDER BY baz DESC\n", 'group by with order by'); +}; + +subtest 'LIMIT and OFFSET' => sub { + $stmt = ns(); + $stmt->from([ 'foo' ]); + $stmt->limit(5); + is($stmt->as_sql, "FROM foo\nLIMIT 5\n"); + $stmt->offset(10); + is($stmt->as_sql, "FROM foo\nLIMIT 5 OFFSET 10\n"); + $stmt->limit(" 15g"); ## Non-numerics should cause an error + { + my $sql = eval { $stmt->as_sql }; + like($@, qr/Non-numerics/, "bogus limit causes as_sql assertion"); + } +}; + +subtest 'WHERE' => sub { + $stmt = ns(); $stmt->add_where(foo => 'bar'); + is($stmt->as_sql_where, "WHERE (foo = ?)\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar'); + + $stmt = ns(); $stmt->add_where(foo => [ 'bar', 'baz' ]); + is($stmt->as_sql_where, "WHERE (foo IN (?,?))\n"); + is(scalar @{ $stmt->bind }, 2); + is($stmt->bind->[0], 'bar'); + is($stmt->bind->[1], 'baz'); + + $stmt = ns(); $stmt->add_where(foo => { op => 'IN', value => ['bar'] }); + is($stmt->as_sql_where, "WHERE (foo IN (?))\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar'); + + $stmt = ns(); $stmt->add_where(foo => { op => 'NOT IN', value => ['bar'] }); + is($stmt->as_sql_where, "WHERE (foo NOT IN (?))\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar'); + + $stmt = ns(); $stmt->add_where(foo => { op => 'BETWEEN', value => ['bar', 'baz'] }); + is($stmt->as_sql_where, "WHERE (foo BETWEEN ? AND ?)\n"); + is(scalar @{ $stmt->bind }, 2); + is($stmt->bind->[0], 'bar'); + is($stmt->bind->[1], 'baz'); + + $stmt = ns(); $stmt->add_where(foo => { op => 'LIKE', value => 'bar%' }); + is($stmt->as_sql_where, "WHERE (foo LIKE ?)\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar%'); + + $stmt = ns(); $stmt->add_where(foo => { op => '!=', value => 'bar' }); + is($stmt->as_sql_where, "WHERE (foo != ?)\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar'); + + $stmt = ns(); $stmt->add_where(foo => { column => 'bar', op => '!=', value => 'bar' }); + is($stmt->as_sql_where, "WHERE (bar != ?)\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'bar'); + + $stmt = ns(); + $stmt->add_where(foo => 'bar'); + $stmt->add_where(baz => 'quux'); + is($stmt->as_sql_where, "WHERE (foo = ?) AND (baz = ?)\n"); + is(scalar @{ $stmt->bind }, 2); + is($stmt->bind->[0], 'bar'); + is($stmt->bind->[1], 'quux'); + + $stmt = ns(); + $stmt->add_where(foo => [ { op => '>', value => 'bar' }, + { op => '<', value => 'baz' } ]); + is($stmt->as_sql_where, "WHERE ((foo > ?) OR (foo < ?))\n"); + is(scalar @{ $stmt->bind }, 2); + is($stmt->bind->[0], 'bar'); + is($stmt->bind->[1], 'baz'); + + $stmt = ns(); + $stmt->add_where(foo => [ -and => { op => '>', value => 'bar' }, + { op => '<', value => 'baz' } ]); + is($stmt->as_sql_where, "WHERE ((foo > ?) AND (foo < ?))\n"); + is(scalar @{ $stmt->bind }, 2); + is($stmt->bind->[0], 'bar'); + is($stmt->bind->[1], 'baz'); + + $stmt = ns(); + $stmt->add_where(foo => [ -and => 'foo', 'bar', 'baz']); + is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); + is(scalar @{ $stmt->bind }, 3); + is($stmt->bind->[0], 'foo'); + is($stmt->bind->[1], 'bar'); + is($stmt->bind->[2], 'baz'); + + $stmt = ns(); + $stmt->add_where(foo => \['IN (SELECT foo FROM bar WHERE t=?)', 'foo']); + is($stmt->as_sql_where, "WHERE (foo IN (SELECT foo FROM bar WHERE t=?))\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'foo'); + + $stmt = ns(); + $stmt->add_where(foo => { op => 'IN', value => \['(SELECT foo FROM bar WHERE t=?)', 'foo']}); + is($stmt->as_sql_where, "WHERE (foo IN ((SELECT foo FROM bar WHERE t=?)))\n"); + is(scalar @{ $stmt->bind }, 1); + is($stmt->bind->[0], 'foo'); + + $stmt = ns(); + $stmt->add_where(foo => { op => 'IN', value => \'(SELECT foo FROM bar)'}); + is($stmt->as_sql_where, "WHERE (foo IN (SELECT foo FROM bar))\n"); + is(scalar @{ $stmt->bind }, 0); + + $stmt = ns(); + $stmt->add_where(foo => undef); + is($stmt->as_sql_where, "WHERE (foo IS NULL)\n"); + is(scalar @{ $stmt->bind }, 0); + + ## avoid syntax error + $stmt = ns(); + $stmt->add_where(foo => []); + is($stmt->as_sql_where, "WHERE (0 = 1)\n"); # foo IN () + is(scalar @{ $stmt->bind }, 0); -is $stmt->as_sql, "FROM foo INNER JOIN baz b1 ON foo.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON foo.baz_id = b2.baz_id AND b2.quux_id = 2 INNER JOIN foo f1 ON f1.quux_id = quux.q_id\n"; - -# test case for bug found where add_join is called for a table included in the "from". -$stmt->from([ 'foo', 'bar' ]); -$stmt->joins([]); -$stmt->add_join(foo => { type => 'inner', table => 'bar', - condition => 'foo.bar_id = bar.bar_id' }); -is($stmt->as_sql, "FROM foo INNER JOIN bar ON foo.bar_id = bar.bar_id\n"); - -## Testing GROUP BY -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->group({ column => 'baz' }); -is($stmt->as_sql, "FROM foo\nGROUP BY baz\n", 'single bare group by'); - -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->group({ column => 'baz', desc => 'DESC' }); -is($stmt->as_sql, "FROM foo\nGROUP BY baz DESC\n", 'single group by with desc'); - -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->group([ { column => 'baz' }, { column => 'quux' }, ]); -is($stmt->as_sql, "FROM foo\nGROUP BY baz, quux\n", 'multiple group by'); - -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->group([ { column => 'baz', desc => 'DESC' }, - { column => 'quux', desc => 'DESC' }, ]); -is($stmt->as_sql, "FROM foo\nGROUP BY baz DESC, quux DESC\n", 'multiple group by with desc'); - -## Testing ORDER BY -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->order({ column => 'baz', desc => 'DESC' }); -is($stmt->as_sql, "FROM foo\nORDER BY baz DESC\n", 'single order by'); - -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->order([ { column => 'baz', desc => 'DESC' }, - { column => 'quux', desc => 'ASC' }, ]); -is($stmt->as_sql, "FROM foo\nORDER BY baz DESC, quux ASC\n", 'multiple order by'); - -## Testing GROUP BY plus ORDER BY -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->group({ column => 'quux' }); -$stmt->order({ column => 'baz', desc => 'DESC' }); -is($stmt->as_sql, "FROM foo\nGROUP BY quux\nORDER BY baz DESC\n", 'group by with order by'); - -## Testing LIMIT and OFFSET -$stmt = ns(); -$stmt->from([ 'foo' ]); -$stmt->limit(5); -is($stmt->as_sql, "FROM foo\nLIMIT 5\n"); -$stmt->offset(10); -is($stmt->as_sql, "FROM foo\nLIMIT 5 OFFSET 10\n"); -$stmt->limit(" 15g"); ## Non-numerics should cause an error -{ - my $sql = eval { $stmt->as_sql }; - like($@, qr/Non-numerics/, "bogus limit causes as_sql assertion"); -} - -## Testing WHERE -$stmt = ns(); $stmt->add_where(foo => 'bar'); -is($stmt->as_sql_where, "WHERE (foo = ?)\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar'); - -$stmt = ns(); $stmt->add_where(foo => [ 'bar', 'baz' ]); -is($stmt->as_sql_where, "WHERE (foo IN (?,?))\n"); -is(scalar @{ $stmt->bind }, 2); -is($stmt->bind->[0], 'bar'); -is($stmt->bind->[1], 'baz'); - -$stmt = ns(); $stmt->add_where(foo => { op => 'IN', value => ['bar'] }); -is($stmt->as_sql_where, "WHERE (foo IN (?))\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar'); - -$stmt = ns(); $stmt->add_where(foo => { op => 'NOT IN', value => ['bar'] }); -is($stmt->as_sql_where, "WHERE (foo NOT IN (?))\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar'); - -$stmt = ns(); $stmt->add_where(foo => { op => 'BETWEEN', value => ['bar', 'baz'] }); -is($stmt->as_sql_where, "WHERE (foo BETWEEN ? AND ?)\n"); -is(scalar @{ $stmt->bind }, 2); -is($stmt->bind->[0], 'bar'); -is($stmt->bind->[1], 'baz'); - -$stmt = ns(); $stmt->add_where(foo => { op => 'LIKE', value => 'bar%' }); -is($stmt->as_sql_where, "WHERE (foo LIKE ?)\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar%'); - -$stmt = ns(); $stmt->add_where(foo => { op => '!=', value => 'bar' }); -is($stmt->as_sql_where, "WHERE (foo != ?)\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar'); - -$stmt = ns(); $stmt->add_where(foo => { column => 'bar', op => '!=', value => 'bar' }); -is($stmt->as_sql_where, "WHERE (bar != ?)\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'bar'); - -$stmt = ns(); -$stmt->add_where(foo => 'bar'); -$stmt->add_where(baz => 'quux'); -is($stmt->as_sql_where, "WHERE (foo = ?) AND (baz = ?)\n"); -is(scalar @{ $stmt->bind }, 2); -is($stmt->bind->[0], 'bar'); -is($stmt->bind->[1], 'quux'); - -$stmt = ns(); -$stmt->add_where(foo => [ { op => '>', value => 'bar' }, - { op => '<', value => 'baz' } ]); -is($stmt->as_sql_where, "WHERE ((foo > ?) OR (foo < ?))\n"); -is(scalar @{ $stmt->bind }, 2); -is($stmt->bind->[0], 'bar'); -is($stmt->bind->[1], 'baz'); - -$stmt = ns(); -$stmt->add_where(foo => [ -and => { op => '>', value => 'bar' }, - { op => '<', value => 'baz' } ]); -is($stmt->as_sql_where, "WHERE ((foo > ?) AND (foo < ?))\n"); -is(scalar @{ $stmt->bind }, 2); -is($stmt->bind->[0], 'bar'); -is($stmt->bind->[1], 'baz'); - -$stmt = ns(); -$stmt->add_where(foo => [ -and => 'foo', 'bar', 'baz']); -is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); -is(scalar @{ $stmt->bind }, 3); -is($stmt->bind->[0], 'foo'); -is($stmt->bind->[1], 'bar'); -is($stmt->bind->[2], 'baz'); - -$stmt = ns(); -$stmt->add_where(foo => \['IN (SELECT foo FROM bar WHERE t=?)', 'foo']); -is($stmt->as_sql_where, "WHERE (foo IN (SELECT foo FROM bar WHERE t=?))\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'foo'); - -$stmt = ns(); -$stmt->add_where(foo => { op => 'IN', value => \['(SELECT foo FROM bar WHERE t=?)', 'foo']}); -is($stmt->as_sql_where, "WHERE (foo IN ((SELECT foo FROM bar WHERE t=?)))\n"); -is(scalar @{ $stmt->bind }, 1); -is($stmt->bind->[0], 'foo'); - -$stmt = ns(); -$stmt->add_where(foo => { op => 'IN', value => \'(SELECT foo FROM bar)'}); -is($stmt->as_sql_where, "WHERE (foo IN (SELECT foo FROM bar))\n"); -is(scalar @{ $stmt->bind }, 0); - -$stmt = ns(); -$stmt->add_where(foo => undef); -is($stmt->as_sql_where, "WHERE (foo IS NULL)\n"); -is(scalar @{ $stmt->bind }, 0); - -## avoid syntax error -$stmt = ns(); -$stmt->add_where(foo => []); -is($stmt->as_sql_where, "WHERE (0 = 1)\n"); # foo IN () -is(scalar @{ $stmt->bind }, 0); - -$stmt = ns(); -$stmt->add_complex_where([]); -is($stmt->as_sql_where, ""); # no WHERE without expression -is(scalar @{ $stmt->bind }, 0); - -$stmt = ns(); -$stmt->add_complex_where([[]]); -is($stmt->as_sql_where, ""); # no WHERE without expression -is(scalar @{ $stmt->bind }, 0); - -$stmt = ns(); -$stmt->add_complex_where([{}]); -is($stmt->as_sql_where, ""); # no WHERE without expression -is(scalar @{ $stmt->bind }, 0); - -$stmt = ns(); -$stmt->add_complex_where([{id => 1}, {}]); -is($stmt->as_sql_where, "WHERE ((id = ?))\n"); # no empty expression -is(scalar @{ $stmt->bind }, 1); - -$stmt = ns(); -$stmt->add_complex_where([{id => 1}, []]); -is($stmt->as_sql_where, "WHERE ((id = ?))\n"); # no empty expression -is(scalar @{ $stmt->bind }, 1); - -## regression bug. modified parameters -my %terms = ( foo => [-and => 'foo', 'bar', 'baz']); -$stmt = ns(); -$stmt->add_where(%terms); -is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); -$stmt->add_where(%terms); -is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?)) AND ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); - -## as_escape -$stmt = ns(); -$stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => '\\' }); -is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\')\n"); -is($stmt->bind->[0], '100%'); # escape doesn't automatically escape the value -$stmt = ns(); -$stmt->add_where(foo => { op => 'LIKE', value => '100\\%', escape => '\\' }); -is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\')\n"); -is($stmt->bind->[0], '100\\%'); -$stmt = ns(); -$stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => '!' }); -is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '!')\n"); -$stmt = ns(); -$stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => "''" }); -is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '''')\n"); -$stmt = ns(); -$stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => "\\'" }); -is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\'')\n"); -$stmt = ns(); -eval { $stmt->add_where(foo => { op => 'LIKE', value => '_', escape => "!!!" }); }; -like($@, qr/length/, 'right error'); - -$stmt = ns(); -$stmt->add_select(foo => 'foo'); -$stmt->add_select('bar'); -$stmt->from([ qw( baz ) ]); -is($stmt->as_sql, "SELECT foo, bar\nFROM baz\n"); - -$stmt = ns(); -$stmt->add_select('f.foo' => 'foo'); -$stmt->add_select('COUNT(*)' => 'count'); -$stmt->from([ qw( baz ) ]); -is($stmt->as_sql, "SELECT f.foo, COUNT(*) count\nFROM baz\n"); -my $map = $stmt->select_map; -is(scalar(keys %$map), 2); -is($map->{'f.foo'}, 'foo'); -is($map->{'COUNT(*)'}, 'count'); - -# HAVING -$stmt = ns(); -$stmt->add_select(foo => 'foo'); -$stmt->add_select('COUNT(*)' => 'count'); -$stmt->from([ qw(baz) ]); -$stmt->add_where(foo => 1); -$stmt->group({ column => 'baz' }); -$stmt->order({ column => 'foo', desc => 'DESC' }); -$stmt->limit(2); -$stmt->add_having(count => 2); - -is($stmt->as_sql, <add_complex_where([]); + is($stmt->as_sql_where, ""); # no WHERE without expression + is(scalar @{ $stmt->bind }, 0); + + $stmt = ns(); + $stmt->add_complex_where([[]]); + is($stmt->as_sql_where, ""); # no WHERE without expression + is(scalar @{ $stmt->bind }, 0); + + $stmt = ns(); + $stmt->add_complex_where([{}]); + is($stmt->as_sql_where, ""); # no WHERE without expression + is(scalar @{ $stmt->bind }, 0); + + $stmt = ns(); + $stmt->add_complex_where([{id => 1}, {}]); + is($stmt->as_sql_where, "WHERE ((id = ?))\n"); # no empty expression + is(scalar @{ $stmt->bind }, 1); + + $stmt = ns(); + $stmt->add_complex_where([{id => 1}, []]); + is($stmt->as_sql_where, "WHERE ((id = ?))\n"); # no empty expression + is(scalar @{ $stmt->bind }, 1); +}; + +subtest 'regression bug. modified parameters' => sub { + my %terms = ( foo => [-and => 'foo', 'bar', 'baz']); + $stmt = ns(); + $stmt->add_where(%terms); + is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); + $stmt->add_where(%terms); + is($stmt->as_sql_where, "WHERE ((foo = ?) AND (foo = ?) AND (foo = ?)) AND ((foo = ?) AND (foo = ?) AND (foo = ?))\n"); +}; + +subtest 'as_escape' => sub { + $stmt = ns(); + $stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => '\\' }); + is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\')\n"); + is($stmt->bind->[0], '100%'); # escape doesn't automatically escape the value + $stmt = ns(); + $stmt->add_where(foo => { op => 'LIKE', value => '100\\%', escape => '\\' }); + is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\')\n"); + is($stmt->bind->[0], '100\\%'); + $stmt = ns(); + $stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => '!' }); + is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '!')\n"); + $stmt = ns(); + $stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => "''" }); + is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '''')\n"); + $stmt = ns(); + $stmt->add_where(foo => { op => 'LIKE', value => '100%', escape => "\\'" }); + is($stmt->as_sql_where, "WHERE (foo LIKE ? ESCAPE '\\'')\n"); + $stmt = ns(); + eval { $stmt->add_where(foo => { op => 'LIKE', value => '_', escape => "!!!" }); }; + like($@, qr/length/, 'right error'); +}; + +subtest 'entire SELECT statement' => sub { + $stmt = ns(); + $stmt->add_select(foo => 'foo'); + $stmt->add_select('bar'); + $stmt->from([ qw( baz ) ]); + is($stmt->as_sql, "SELECT foo, bar\nFROM baz\n"); +}; + +subtest 'SQL functions in select list' => sub { + + subtest 'single function' => sub { + $stmt = ns(); + $stmt->add_select('f.foo' => 'foo'); + $stmt->add_select('COUNT(*)' => 'count'); + $stmt->from([ qw( baz ) ]); + is($stmt->as_sql, "SELECT f.foo, COUNT(*) count\nFROM baz\n"); + my $map = $stmt->select_map; + is(scalar(keys %$map), 2); + is_deeply($map, {'f.foo' => 'foo', 'COUNT(*)' => 'count'}, 'right map'); + }; + + subtest 'multiple functions without alias' => sub { + $stmt = ns(); + $stmt->add_select('count(foo)'); + $stmt->add_select('count(bar)'); + $stmt->from([qw( baz )]); + is($stmt->as_sql, "SELECT count(foo), count(bar)\nFROM baz\n"); + my $map = $stmt->select_map; + is(scalar(keys %$map), 2); + is_deeply($map, {'count(foo)' => 'count(foo)', 'count(bar)' => 'count(bar)'}, 'right map'); + }; + + subtest 'multiple functions with alias' => sub { + $stmt = ns(); + $stmt->add_select('count(foo)', 'count1'); + $stmt->add_select('count(bar)', 'count2'); + $stmt->from([qw( baz )]); + is($stmt->as_sql, "SELECT count(foo) count1, count(bar) count2\nFROM baz\n"); + my $map = $stmt->select_map; + is(scalar(keys %$map), 2); + is_deeply($map, {'count(foo)' => 'count1', 'count(bar)' => 'count2'}, 'right map'); + }; +}; + +subtest 'HAVING' => sub { + # HAVING + $stmt = ns(); + $stmt->add_select(foo => 'foo'); + $stmt->add_select('COUNT(*)' => 'count'); + $stmt->from([ qw(baz) ]); + $stmt->add_where(foo => 1); + $stmt->group({ column => 'baz' }); + $stmt->order({ column => 'foo', desc => 'DESC' }); + $stmt->limit(2); + $stmt->add_having(count => 2); + + is($stmt->as_sql, < sub { + $stmt = ns(); + $stmt->add_select(foo => 'foo'); + $stmt->from([ qw(baz) ]); + is($stmt->as_sql, "SELECT foo\nFROM baz\n", "DISTINCT is absent by default"); + $stmt->distinct(1); + is($stmt->as_sql, "SELECT DISTINCT foo\nFROM baz\n", "we can turn on DISTINCT"); +}; + +subtest 'index hint' => sub { + $stmt = ns(); + $stmt->add_select(foo => 'foo'); + $stmt->from([ qw(baz) ]); + is($stmt->as_sql, "SELECT foo\nFROM baz\n", "index hint is absent by default"); + $stmt->add_index_hint('baz' => { type => 'USE', list => ['index_hint']}); + is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint)\n", "we can turn on USE INDEX"); + + # index hint with joins + $stmt->joins([]); + $stmt->from([]); + $stmt->add_join(baz => { type => 'inner', table => 'baz', + condition => 'baz.baz_id = foo.baz_id' }); + is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz ON baz.baz_id = foo.baz_id\n", 'USE INDEX with JOIN'); + $stmt->from([]); + $stmt->joins([]); + $stmt->add_join(baz => [ + { type => 'inner', table => 'baz b1', + condition => 'baz.baz_id = b1.baz_id AND b1.quux_id = 1' }, + { type => 'left', table => 'baz b2', + condition => 'baz.baz_id = b2.baz_id AND b2.quux_id = 2' }, + ]); + is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz b1 ON baz.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON baz.baz_id = b2.baz_id AND b2.quux_id = 2\n", 'USE INDEX with JOINs'); +}; -# DISTINCT -$stmt = ns(); -$stmt->add_select(foo => 'foo'); -$stmt->from([ qw(baz) ]); -is($stmt->as_sql, "SELECT foo\nFROM baz\n", "DISTINCT is absent by default"); -$stmt->distinct(1); -is($stmt->as_sql, "SELECT DISTINCT foo\nFROM baz\n", "we can turn on DISTINCT"); - -# index hint -$stmt = ns(); -$stmt->add_select(foo => 'foo'); -$stmt->from([ qw(baz) ]); -is($stmt->as_sql, "SELECT foo\nFROM baz\n", "index hint is absent by default"); -$stmt->add_index_hint('baz' => { type => 'USE', list => ['index_hint']}); -is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint)\n", "we can turn on USE INDEX"); - -# index hint with joins -$stmt->joins([]); -$stmt->from([]); -$stmt->add_join(baz => { type => 'inner', table => 'baz', - condition => 'baz.baz_id = foo.baz_id' }); -is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz ON baz.baz_id = foo.baz_id\n", 'USE INDEX with JOIN'); -$stmt->from([]); -$stmt->joins([]); -$stmt->add_join(baz => [ - { type => 'inner', table => 'baz b1', - condition => 'baz.baz_id = b1.baz_id AND b1.quux_id = 1' }, - { type => 'left', table => 'baz b2', - condition => 'baz.baz_id = b2.baz_id AND b2.quux_id = 2' }, +subtest 'comments' => sub { + $stmt = ns(); + $stmt->add_select(foo => 'foo'); + $stmt->from([ qw(baz) ]); + $stmt->comment("mycomment"); + is($stmt->as_sql, "SELECT foo\nFROM baz\n-- mycomment"); + + $stmt->comment("\nbad\n\nmycomment"); + is($stmt->as_sql, "SELECT foo\nFROM baz\n-- bad", "correctly untainted"); + + $stmt->comment("G\\G"); + is($stmt->as_sql, "SELECT foo\nFROM baz\n-- G", "correctly untainted"); +}; + +subtest 'complex WHERE' => sub { + $stmt = ns(); + $stmt->add_complex_where([ + { foo => 'foo_value' }, + { bar => 'bar_value' }, ]); -is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz b1 ON baz.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON baz.baz_id = b2.baz_id AND b2.quux_id = 2\n", 'USE INDEX with JOINs'); - -$stmt = ns(); -$stmt->add_select(foo => 'foo'); -$stmt->from([ qw(baz) ]); -$stmt->comment("mycomment"); -is($stmt->as_sql, "SELECT foo\nFROM baz\n-- mycomment"); - -$stmt->comment("\nbad\n\nmycomment"); -is($stmt->as_sql, "SELECT foo\nFROM baz\n-- bad", "correctly untainted"); - -$stmt->comment("G\\G"); -is($stmt->as_sql, "SELECT foo\nFROM baz\n-- G", "correctly untainted"); - -## Testing complex WHERE -$stmt = ns(); -$stmt->add_complex_where([ - { foo => 'foo_value' }, - { bar => 'bar_value' }, -]); -is($stmt->as_sql_where, "WHERE ((foo = ?)) AND ((bar = ?))\n"); - -$stmt = ns(); -my @terms = ( - { foo => 'foo_value' }, - { - bar => [ - { op => 'LIKE', value => 'bar1%' }, - { op => 'LIKE', value => 'bar2%' }, - ], - baz => 'baz_value', - } -); -$stmt->add_complex_where(\@terms); -is( - $stmt->as_sql_where, - (keys(%{$terms[1]}))[0] eq 'bar' - ? "WHERE ((foo = ?)) AND (((bar LIKE ?) OR (bar LIKE ?)) AND (baz = ?))\n" - : "WHERE ((foo = ?)) AND ((baz = ?) AND ((bar LIKE ?) OR (bar LIKE ?)))\n" -); + is($stmt->as_sql_where, "WHERE ((foo = ?)) AND ((bar = ?))\n"); + + $stmt = ns(); + my @terms = ( + { foo => 'foo_value' }, + { + bar => [ + { op => 'LIKE', value => 'bar1%' }, + { op => 'LIKE', value => 'bar2%' }, + ], + baz => 'baz_value', + } + ); + $stmt->add_complex_where(\@terms); + is( + $stmt->as_sql_where, + (keys(%{$terms[1]}))[0] eq 'bar' + ? "WHERE ((foo = ?)) AND (((bar LIKE ?) OR (bar LIKE ?)) AND (baz = ?))\n" + : "WHERE ((foo = ?)) AND ((baz = ?) AND ((bar LIKE ?) OR (bar LIKE ?)))\n" + ); +}; subtest 'scalar reference' => sub { $stmt = ns(); @@ -407,3 +452,5 @@ subtest 'scalar reference' => sub { }; sub ns { Data::ObjectDriver::SQL->new } + +done_testing; diff --git a/t/lib/sql/Blog.pm b/t/lib/sql/Blog.pm new file mode 100644 index 0000000..807bc71 --- /dev/null +++ b/t/lib/sql/Blog.pm @@ -0,0 +1,17 @@ +# $Id$ + +package Blog; +use strict; +use warnings; +use base 'Data::ObjectDriver::BaseObject'; +use Data::ObjectDriver::Driver::DBI; +use DodTestUtil; + +__PACKAGE__->install_properties({ + columns => ['id', 'parent_id', 'name'], + datasource => 'blog', + primary_key => 'id', + driver => Data::ObjectDriver::Driver::DBI->new(dsn => DodTestUtil::dsn('global')), +}); + +1; diff --git a/t/lib/sql/Entry.pm b/t/lib/sql/Entry.pm new file mode 100644 index 0000000..90bb474 --- /dev/null +++ b/t/lib/sql/Entry.pm @@ -0,0 +1,17 @@ +# $Id$ + +package Entry; +use strict; +use warnings; +use base 'Data::ObjectDriver::BaseObject'; +use Data::ObjectDriver::Driver::DBI; +use DodTestUtil; + +__PACKAGE__->install_properties({ + columns => ['id', 'blog_id', 'title', 'text'], + datasource => 'entry', + primary_key => 'id', + driver => Data::ObjectDriver::Driver::DBI->new(dsn => DodTestUtil::dsn('global')), +}); + +1; diff --git a/t/schemas/blog.sql b/t/schemas/blog.sql new file mode 100644 index 0000000..82ed167 --- /dev/null +++ b/t/schemas/blog.sql @@ -0,0 +1,5 @@ +CREATE TABLE blog ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + parent_id INTEGER, + name VARCHAR(50) +) diff --git a/t/schemas/entry.sql b/t/schemas/entry.sql new file mode 100644 index 0000000..7328ab7 --- /dev/null +++ b/t/schemas/entry.sql @@ -0,0 +1,6 @@ +CREATE TABLE entry ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER, + title VARCHAR(50), + text MEDIUMTEXT +)