}
-# Slow but ANSI standard Limit/Offset support. DB2 uses this
+# ANSI standard Limit/Offset implementation. DB2 and MSSQL use this
sub _RowNumberOver {
my ($self, $sql, $order, $rows, $offset ) = @_;
- $offset += 1;
- my $last = $rows + $offset - 1;
- my ( $order_by ) = $self->_order_by( $order );
+ # get the order_by only (or make up an order if none exists)
+ my $order_by = $self->_order_by(
+ (delete $order->{order_by}) || $self->_rno_default_order
+ );
- $sql = <<"SQL";
-SELECT * FROM
-(
- SELECT Q1.*, ROW_NUMBER() OVER( ) AS ROW_NUM FROM (
- $sql
- $order_by
- ) Q1
-) Q2
-WHERE ROW_NUM BETWEEN $offset AND $last
+ # whatever is left
+ my $group_having = $self->_order_by($order);
-SQL
+ $sql = sprintf (<<'EOS', $order_by, $sql, $group_having, $offset + 1, $offset + $rows, );
+
+SELECT * FROM (
+ SELECT orig_query.*, ROW_NUMBER() OVER(%s ) AS rno__row__index FROM (%s%s) orig_query
+) rno_subq WHERE rno__row__index BETWEEN %d AND %d
+EOS
+
+ $sql =~ s/\s*\n\s*/ /g; # easier to read in the debugger
return $sql;
}
-# Crappy Top based Limit/Offset support. MSSQL uses this currently,
-# but may have to switch to RowNumberOver one day
+# some databases are happy with OVER (), some need OVER (ORDER BY (SELECT (1)) )
+sub _rno_default_order {
+ return undef;
+}
+
+# Crappy Top based Limit/Offset support. Legacy from MSSQL.
sub _Top {
my ( $self, $sql, $order, $rows, $offset ) = @_;
if (
( $attrs->{rows} && keys %{$attrs->{collapse}} )
||
- ( $attrs->{group_by} && @{$attrs->{group_by}} &&
- $attrs->{_prefetch_select} && @{$attrs->{_prefetch_select}} )
+ ( $attrs->{group_by}
+ &&
+ @{$attrs->{group_by}}
+ &&
+ $attrs->{_prefetch_select}
+ &&
+ @{$attrs->{_prefetch_select}}
+ )
) {
+
($ident, $select, $where, $attrs)
= $self->_adjust_select_args_for_complex_prefetch ($ident, $select, $where, $attrs);
}
+
+ elsif (
+ ($attrs->{rows} || $attrs->{offset})
+ &&
+ $sql_maker->limit_dialect eq 'RowNumberOver'
+ &&
+ (ref $ident eq 'ARRAY' && @$ident > 1) # indicates a join
+ &&
+ scalar $sql_maker->_order_by_chunks ($attrs->{order_by})
+ ) {
+ # the RNO limit dialect above mangles the SQL such that the join gets lost
+ # wrap a subquery here
+
+ push @limit, delete @{$attrs}{qw/rows offset/};
+
+ my $subq = $self->_select_args_to_query (
+ $ident,
+ $select,
+ $where,
+ $attrs,
+ );
+
+ $ident = {
+ -alias => $attrs->{alias},
+ -source_handle => $ident->[0]{-source_handle},
+ $attrs->{alias} => $subq,
+ };
+
+ # all part of the subquery now
+ delete @{$attrs}{qw/order_by group_by having/};
+ $where = undef;
+ }
+
elsif (! $attrs->{software_limit} ) {
push @limit, $attrs->{rows}, $attrs->{offset};
}
sub last_insert_id { shift->_identity }
+#
+# MSSQL is retarded wrt ordered subselects. One needs to add a TOP 100%
+# to *all* subqueries, do it here.
+#
+sub _select_args_to_query {
+ my $self = shift;
+
+ my ($sql, $prep_bind, @rest) = $self->next::method (@_);
+
+ # see if this is an ordered subquery
+ my $attrs = $_[3];
+ if ( scalar $self->sql_maker->_order_by_chunks ($attrs->{order_by}) ) {
+ $sql =~ s/^ \s* SELECT \s/SELECT TOP 100 PERCENT /xi;
+ }
+
+ return wantarray
+ ? ($sql, $prep_bind, @rest)
+ : \[ "($sql)", @$prep_bind ]
+ ;
+}
+
+
# savepoint syntax is the same as in Sybase ASE
sub _svp_begin {
sub sqlt_type { 'SQLServer' }
-sub _sql_maker_opts {
- my ( $self, $opts ) = @_;
+sub _get_mssql_version {
+ my $self = shift;
+
+ my $data = $self->_get_dbh->selectrow_hashref('xp_msver ProductVersion');
+
+ if ($data->{Character_Value} =~ /^(\d+)\./) {
+ return $1;
+ } else {
+ $self->throw_exception(q{Your ProductVersion's Character_Value is missing or malformed!});
+ }
+}
+
+sub sql_maker {
+ my $self = shift;
+
+ unless ($self->_sql_maker) {
+ unless ($self->{_sql_maker_opts}{limit_dialect}) {
+ my $version = $self->_get_mssql_version;
+
+ $self->{_sql_maker_opts} = {
+ limit_dialect => ($version >= 9 ? 'RowNumberOver' : 'Top'),
+ %{$self->{_sql_maker_opts}||{}}
+ };
+ }
- if ( $opts ) {
- $self->{_sql_maker_opts} = { %$opts };
+ my $maker = $self->next::method (@_);
}
- return { limit_dialect => 'Top', %{$self->{_sql_maker_opts}||{}} };
+ return $self->_sql_maker;
}
1;
isa_ok( $schema->storage, 'DBIx::Class::Storage::DBI::ODBC::Microsoft_SQL_Server' );
+{
+ my $schema2 = $schema->connect ($schema->storage->connect_info);
+ ok (! $schema2->storage->connected, 'a re-connected cloned schema starts unconnected');
+}
+
$schema->storage->dbh_do (sub {
my ($storage, $dbh) = @_;
eval { $dbh->do("DROP TABLE artist") };
]);
}, 'populate without PKs supplied ok' );
+# make sure ordered subselects work
+{
+ my $book_owner_ids = $schema->resultset ('BooksInLibrary')
+ ->search ({}, { join => 'owner', distinct => 1, order_by => { -desc => 'owner'} })
+ ->get_column ('owner');
+
+ my $owners = $schema->resultset ('Owners')->search ({
+ id => { -in => $book_owner_ids->as_query }
+ });
+
+ is ($owners->count, 8, 'Correct amount of book owners');
+ is ($owners->all, 8, 'Correct amount of book owner objects');
+}
+
#
# try a prefetch on tables with identically named columns
#
{
# try a ->has_many direction
- my $owners = $schema->resultset ('Owners')->search ({
- 'books.id' => { '!=', undef }
- }, {
+ my $owners = $schema->resultset ('Owners')->search (
+ {
+ 'books.id' => { '!=', undef },
+ 'me.name' => { '!=', 'somebogusstring' },
+ },
+ {
prefetch => 'books',
- order_by => 'name',
+ order_by => { -asc => \['name + ?', [ test => 'xxx' ]] }, # test bindvar propagation
rows => 3, # 8 results total
- });
+ },
+ );
+
+ my ($sql, @bind) = @${$owners->page(3)->as_query};
+ is_deeply (
+ \@bind,
+ [ ([ 'me.name' => 'somebogusstring' ], [ test => 'xxx' ]) x 2 ], # double because of the prefetch subq
+ );
is ($owners->page(1)->all, 3, 'has_many prefetch returns correct number of rows');
is ($owners->page(1)->count, 3, 'has-many prefetch returns correct count');
- TODO: {
- local $TODO = 'limit past end of resultset problem';
- is ($owners->page(3)->all, 2, 'has_many prefetch returns correct number of rows');
- is ($owners->page(3)->count, 2, 'has-many prefetch returns correct count');
- is ($owners->page(3)->count_rs->next, 2, 'has-many prefetch returns correct count_rs');
-
- # make sure count does not become overly complex
- is_same_sql_bind (
- $owners->page(3)->count_rs->as_query,
- '(
- SELECT COUNT( * )
- FROM (
- SELECT TOP 3 [me].[id]
- FROM [owners] [me]
- LEFT JOIN [books] [books] ON [books].[owner] = [me].[id]
- WHERE ( [books].[id] IS NOT NULL )
- GROUP BY [me].[id]
- ORDER BY [me].[id] DESC
- ) [count_subq]
- )',
- [],
- );
- }
+ is ($owners->page(3)->all, 2, 'has_many prefetch returns correct number of rows');
+ is ($owners->page(3)->count, 2, 'has-many prefetch returns correct count');
+ is ($owners->page(3)->count_rs->next, 2, 'has-many prefetch returns correct count_rs');
+
# try a ->belongs_to direction (no select collapse, group_by should work)
- my $books = $schema->resultset ('BooksInLibrary')->search ({
+ my $books = $schema->resultset ('BooksInLibrary')->search (
+ {
'owner.name' => [qw/wiggle woggle/],
- }, {
+ },
+ {
distinct => 1,
+ having => \['1 = ?', [ test => 1 ] ], #test having propagation
prefetch => 'owner',
rows => 2, # 3 results total
order_by => { -desc => 'owner' },
- # there is no sane way to order by the right side of a grouped prefetch currently :(
- #order_by => { -desc => 'owner.name' },
- });
-
+ },
+ );
+
+ ($sql, @bind) = @${$books->page(3)->as_query};
+ is_deeply (
+ \@bind,
+ [
+ # inner
+ [ 'owner.name' => 'wiggle' ], [ 'owner.name' => 'woggle' ], [ source => 'Library' ], [ test => '1' ],
+ # outer
+ [ 'owner.name' => 'wiggle' ], [ 'owner.name' => 'woggle' ], [ source => 'Library' ],
+ ],
+ );
is ($books->page(1)->all, 2, 'Prefetched grouped search returns correct number of rows');
is ($books->page(1)->count, 2, 'Prefetched grouped search returns correct count');
- TODO: {
- local $TODO = 'limit past end of resultset problem';
- is ($books->page(2)->all, 1, 'Prefetched grouped search returns correct number of rows');
- is ($books->page(2)->count, 1, 'Prefetched grouped search returns correct count');
- is ($books->page(2)->count_rs->next, 1, 'Prefetched grouped search returns correct count_rs');
-
- # make sure count does not become overly complex (FIXME - the distinct-induced group_by is incorrect)
- is_same_sql_bind (
- $books->page(2)->count_rs->as_query,
- '(
- SELECT COUNT( * )
- FROM (
- SELECT TOP 2 [me].[id]
- FROM [books] [me]
- JOIN [owners] [owner] ON [owner].[id] = [me].[owner]
- WHERE ( ( ( [owner].[name] = ? OR [owner].[name] = ? ) AND [source] = ? ) )
- GROUP BY [me].[id], [me].[source], [me].[owner], [me].[title], [me].[price]
- ORDER BY [me].[id] DESC
- ) [count_subq]
- )',
- [
- [ 'owner.name' => 'wiggle' ],
- [ 'owner.name' => 'woggle' ],
- [ 'source' => 'Library' ],
- ],
- );
- }
+ is ($books->page(2)->all, 1, 'Prefetched grouped search returns correct number of rows');
+ is ($books->page(2)->count, 1, 'Prefetched grouped search returns correct count');
+ is ($books->page(2)->count_rs->next, 1, 'Prefetched grouped search returns correct count_rs');
+}
+
+# make sure right-join-side ordering limit works
+{
+ my $rs = $schema->resultset ('BooksInLibrary')->search (
+ {
+ 'owner.name' => [qw/wiggle woggle/],
+ },
+ {
+ join => 'owner',
+ order_by => { -desc => 'owner.name' },
+ }
+ );
+
+ is ($rs->all, 3, 'Correct amount of objects from right-sorted joined resultset');
+ my $limited_rs = $rs->search ({}, {rows => 3, offset => 1});
+ is ($limited_rs->count, 2, 'Correct count of limited right-sorted joined resultset');
+ is ($limited_rs->count_rs->next, 2, 'Correct count_rs of limited right-sorted joined resultset');
+ is ($limited_rs->all, 2, 'Correct amount of objects from limited right-sorted joined resultset');
+
+ is_deeply (
+ [map { $_->name } ($limited_rs->search_related ('owner')->all) ],
+ [qw/woggle wiggle/], # there is 1 woggle library book and 2 wiggle books, the limit gets us one of each
+ 'Rows were properly ordered'
+ );
}
done_testing;