0193753a62d21448738c6898961bd67c9479107c
[dbsrgits/DBIx-Class.git] / t / 746mssql.t
1 use strict;
2 use warnings;
3
4 use Test::More;
5 use Test::Exception;
6 use Try::Tiny;
7 use DBIx::Class::SQLMaker::LimitDialects;
8 use DBIx::Class::Optional::Dependencies ();
9 use lib qw(t/lib);
10 use DBICTest;
11 use DBIC::SqlMakerTest;
12
13 plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mssql_odbc')
14   unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mssql_odbc');
15
16 my $OFFSET = DBIx::Class::SQLMaker::LimitDialects->__offset_bindtype;
17 my $TOTAL  = DBIx::Class::SQLMaker::LimitDialects->__total_bindtype;
18
19 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/};
20
21 plan skip_all => 'Set $ENV{DBICTEST_MSSQL_ODBC_DSN}, _USER and _PASS to run this test'
22   unless ($dsn && $user);
23
24 {
25   my $srv_ver = DBICTest::Schema->connect($dsn, $user, $pass)->storage->_server_info->{dbms_version};
26   ok ($srv_ver, 'Got a test server version on fresh schema: ' . ($srv_ver||'???') );
27 }
28
29 DBICTest::Schema->load_classes('ArtistGUID');
30 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
31
32 {
33   no warnings 'redefine';
34   my $connect_count = 0;
35   my $orig_connect = \&DBI::connect;
36   local *DBI::connect = sub { $connect_count++; goto &$orig_connect };
37
38   $schema->storage->ensure_connected;
39
40   is( $connect_count, 1, 'only one connection made');
41 }
42
43 isa_ok( $schema->storage, 'DBIx::Class::Storage::DBI::ODBC::Microsoft_SQL_Server' );
44
45 {
46   my $schema2 = $schema->connect ($schema->storage->connect_info);
47   ok (! $schema2->storage->connected, 'a re-connected cloned schema starts unconnected');
48 }
49
50 $schema->storage->_dbh->disconnect;
51
52 lives_ok {
53   $schema->storage->dbh_do(sub { $_[1]->do('select 1') })
54 } '_ping works';
55
56 my %opts = (
57   use_mars =>
58     { opts => { on_connect_call => 'use_mars' } },
59   use_dynamic_cursors =>
60     { opts => { on_connect_call => 'use_dynamic_cursors' },
61       required => $schema->storage->_using_freetds ? 0 : 1,
62     },
63   use_server_cursors =>
64     { opts => { on_connect_call => 'use_server_cursors' } },
65   NO_OPTION =>
66     { opts => {}, required => 1 },
67 );
68
69 for my $opts_name (keys %opts) {
70   SKIP: {
71     my $opts = $opts{$opts_name}{opts};
72     $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
73
74     try {
75       $schema->storage->ensure_connected
76     }
77     catch {
78       if ($opts{$opts_name}{required}) {
79         BAIL_OUT "on_connect_call option '$opts_name' is not functional: $_";
80       }
81       else {
82         skip
83 "on_connect_call option '$opts_name' not functional in this configuration: $_",
84 1;
85       }
86     };
87
88     $schema->storage->dbh_do (sub {
89         my ($storage, $dbh) = @_;
90         eval { $dbh->do("DROP TABLE artist") };
91         $dbh->do(<<'SQL');
92 CREATE TABLE artist (
93    artistid INT IDENTITY NOT NULL,
94    name VARCHAR(100),
95    rank INT NOT NULL DEFAULT '13',
96    charfield CHAR(10) NULL,
97    primary key(artistid)
98 )
99 SQL
100     });
101
102 # test Auto-PK
103     $schema->resultset('Artist')->search({ name => 'foo' })->delete;
104
105     my $new = $schema->resultset('Artist')->create({ name => 'foo' });
106
107     ok(($new->artistid||0) > 0, "Auto-PK worked for $opts_name");
108
109 # Test multiple active statements
110     SKIP: {
111       skip 'not a multiple active statements configuration', 1
112         if $opts_name eq 'plain';
113
114       my $artist_rs = $schema->resultset('Artist');
115
116       $artist_rs->delete;
117
118       $artist_rs->create({ name => "Artist$_" }) for (1..3);
119
120       my $forward  = $artist_rs->search({},
121         { order_by => { -asc  => 'artistid' } });
122       my $backward = $artist_rs->search({},
123         { order_by => { -desc => 'artistid' } });
124
125       my @map = (
126         [qw/Artist1 Artist3/], [qw/Artist2 Artist2/], [qw/Artist3 Artist1/]
127       );
128       my @result;
129
130       while (my $forward_row = $forward->next) {
131         my $backward_row = $backward->next;
132         push @result, [$forward_row->name, $backward_row->name];
133       }
134
135       is_deeply \@result, \@map, "multiple active statements in $opts_name";
136
137       $artist_rs->delete;
138
139       is($artist_rs->count, 0, '$dbh still viable');
140     }
141
142 # Test populate
143
144     {
145       $schema->storage->dbh_do (sub {
146         my ($storage, $dbh) = @_;
147         eval { $dbh->do("DROP TABLE owners") };
148         eval { $dbh->do("DROP TABLE books") };
149         $dbh->do(<<'SQL');
150 CREATE TABLE books (
151    id INT IDENTITY (1, 1) NOT NULL,
152    source VARCHAR(100),
153    owner INT,
154    title VARCHAR(10),
155    price INT NULL
156 )
157
158 CREATE TABLE owners (
159    id INT IDENTITY (1, 1) NOT NULL,
160    name VARCHAR(100),
161 )
162 SQL
163       });
164
165       lives_ok ( sub {
166         # start a new connection, make sure rebless works
167         my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
168         $schema->populate ('Owners', [
169           [qw/id  name  /],
170           [qw/1   wiggle/],
171           [qw/2   woggle/],
172           [qw/3   boggle/],
173           [qw/4   fRIOUX/],
174           [qw/5   fRUE/],
175           [qw/6   fREW/],
176           [qw/7   fROOH/],
177           [qw/8   fISMBoC/],
178           [qw/9   station/],
179           [qw/10   mirror/],
180           [qw/11   dimly/],
181           [qw/12   face_to_face/],
182           [qw/13   icarus/],
183           [qw/14   dream/],
184           [qw/15   dyrstyggyr/],
185         ]);
186       }, 'populate with PKs supplied ok' );
187
188
189       lives_ok (sub {
190         # start a new connection, make sure rebless works
191         # test an insert with a supplied identity, followed by one without
192         my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
193         for (2, 1) {
194           my $id = $_ * 20 ;
195           $schema->resultset ('Owners')->create ({ id => $id, name => "troglodoogle $id" });
196           $schema->resultset ('Owners')->create ({ name => "troglodoogle " . ($id + 1) });
197         }
198       }, 'create with/without PKs ok' );
199
200       is ($schema->resultset ('Owners')->count, 19, 'owner rows really in db' );
201
202       lives_ok ( sub {
203         # start a new connection, make sure rebless works
204         my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
205         $schema->populate ('BooksInLibrary', [
206           [qw/source  owner title   /],
207           [qw/Library 1     secrets0/],
208           [qw/Library 1     secrets1/],
209           [qw/Eatery  1     secrets2/],
210           [qw/Library 2     secrets3/],
211           [qw/Library 3     secrets4/],
212           [qw/Eatery  3     secrets5/],
213           [qw/Library 4     secrets6/],
214           [qw/Library 5     secrets7/],
215           [qw/Eatery  5     secrets8/],
216           [qw/Library 6     secrets9/],
217           [qw/Library 7     secrets10/],
218           [qw/Eatery  7     secrets11/],
219           [qw/Library 8     secrets12/],
220         ]);
221       }, 'populate without PKs supplied ok' );
222     }
223
224 # test simple, complex LIMIT and limited prefetch support, with both dialects and quote combinations (if possible)
225     for my $dialect (
226       'Top',
227       ($schema->storage->_server_info->{normalized_dbms_version} || 0 ) >= 9
228         ? ('RowNumberOver')
229         : ()
230       ,
231     ) {
232       for my $quoted (0, 1) {
233
234         $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
235             limit_dialect => $dialect,
236             %$opts,
237             $quoted
238               ? ( quote_char => [ qw/ [ ] / ], name_sep => '.' )
239               : ()
240             ,
241           });
242
243         my $test_type = "Dialect:$dialect Quoted:$quoted";
244
245         # basic limit support
246         TODO: {
247           my $art_rs = $schema->resultset ('Artist');
248           $art_rs->delete;
249           $art_rs->create({ name => 'Artist ' . $_ }) for (1..6);
250
251           my $it = $schema->resultset('Artist')->search( {}, {
252             rows => 4,
253             offset => 3,
254             order_by => 'artistid',
255           });
256
257           is( $it->count, 3, "$test_type: LIMIT count ok" );
258
259           local $TODO = "Top-limit does not work when your limit ends up past the resultset"
260             if $dialect eq 'Top';
261
262           is( $it->next->name, 'Artist 4', "$test_type: iterator->next ok" );
263           $it->next;
264           is( $it->next->name, 'Artist 6', "$test_type: iterator->next ok" );
265           is( $it->next, undef, "$test_type: next past end of resultset ok" );
266         }
267
268         # plain ordered subqueries throw
269         throws_ok (sub {
270           $schema->resultset('Owners')->search ({}, { order_by => 'name' })->as_query
271         }, qr/ordered subselect encountered/, "$test_type: Ordered Subselect detection throws ok");
272
273         # make sure ordered subselects *somewhat* work
274         {
275           my $owners = $schema->resultset ('Owners')->search ({}, { order_by => 'name', offset => 2, rows => 3, unsafe_subselect_ok => 1 });
276           my $sealed_owners = $owners->as_subselect_rs;
277
278           is_deeply (
279             [ map { $_->name } ($sealed_owners->all) ],
280             [ map { $_->name } ($owners->all) ],
281             "$test_type: Sort preserved from within a subquery",
282           );
283         }
284
285         # still even with lost order of IN, we should be getting correct
286         # sets
287         {
288           my $owners = $schema->resultset ('Owners')->search ({}, { order_by => 'name', offset => 2, rows => 3, unsafe_subselect_ok => 1 });
289           my $corelated_owners = $owners->result_source->resultset->search (
290             {
291               id => { -in => $owners->get_column('id')->as_query },
292             },
293             {
294               order_by => 'name' #reorder because of what is shown above
295             },
296           );
297
298           is (
299             join ("\x00", map { $_->name } ($corelated_owners->all) ),
300             join ("\x00", map { $_->name } ($owners->all) ),
301             "$test_type: With an outer order_by, everything still matches",
302           );
303         }
304
305         # make sure right-join-side single-prefetch ordering limit works
306         {
307           my $rs = $schema->resultset ('BooksInLibrary')->search (
308             {
309               'owner.name' => { '!=', 'woggle' },
310             },
311             {
312               prefetch => 'owner',
313               order_by => 'owner.name',
314             }
315           );
316           # this is the order in which they should come from the above query
317           my @owner_names = qw/boggle fISMBoC fREW fRIOUX fROOH fRUE wiggle wiggle/;
318
319           is ($rs->all, 8, "$test_type: Correct amount of objects from right-sorted joined resultset");
320           is_deeply (
321             [map { $_->owner->name } ($rs->all) ],
322             \@owner_names,
323             "$test_type: Prefetched rows were properly ordered"
324           );
325
326           my $limited_rs = $rs->search ({}, {rows => 6, offset => 2, unsafe_subselect_ok => 1});
327           is ($limited_rs->count, 6, "$test_type: Correct count of limited right-sorted joined resultset");
328           is ($limited_rs->count_rs->next, 6, "$test_type: Correct count_rs of limited right-sorted joined resultset");
329
330           my $queries;
331           my $orig_debug = $schema->storage->debug;
332           $schema->storage->debugcb(sub { $queries++; });
333           $schema->storage->debug(1);
334
335           is_deeply (
336             [map { $_->owner->name } ($limited_rs->all) ],
337             [@owner_names[2 .. 7]],
338             "$test_type: Prefetch-limited rows were properly ordered"
339           );
340           is ($queries, 1, "$test_type: Only one query with prefetch");
341
342           $schema->storage->debugcb(undef);
343           $schema->storage->debug($orig_debug);
344
345           is_deeply (
346             [map { $_->name } ($limited_rs->search_related ('owner')->all) ],
347             [@owner_names[2 .. 7]],
348             "$test_type: Rows are still properly ordered after search_related",
349           );
350         }
351
352         # try a ->has_many direction with duplicates
353         my $owners = $schema->resultset ('Owners')->search (
354           {
355             'books.id' => { '!=', undef },
356             'me.name' => { '!=', 'somebogusstring' },
357           },
358           {
359             prefetch => 'books',
360             order_by => [ { -asc => \['name + ?', [ test => 'xxx' ]] }, 'me.id' ], # test bindvar propagation
361             group_by => [ map { "me.$_" } $schema->source('Owners')->columns ], # the literal order_by requires an explicit group_by
362             rows     => 3,  # 8 results total
363             unsafe_subselect_ok => 1,
364           },
365         );
366
367         my ($sql, @bind) = @${$owners->page(3)->as_query};
368         is_same_bind (
369           \@bind,
370           [
371             ($dialect eq 'Top' ? [ { dbic_colname => 'test' } => 'xxx' ] : ()), # the extra re-order bind
372             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.name' }
373               => 'somebogusstring' ],
374             [ { dbic_colname => 'test' }
375               => 'xxx' ],
376             ($dialect ne 'Top' ? ( [ $OFFSET => 7 ], [ $TOTAL => 9 ] ) : ()), # parameterised RNO
377             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.name' }
378               => 'somebogusstring' ],
379             [ { dbic_colname => 'test' }
380               => 'xxx' ],
381           ],
382         );
383
384         is ($owners->page(1)->all, 3, "$test_type: has_many prefetch returns correct number of rows");
385         is ($owners->page(1)->count, 3, "$test_type: has-many prefetch returns correct count");
386
387         is ($owners->page(3)->count, 2, "$test_type: has-many prefetch returns correct count");
388         TODO: {
389           local $TODO = "Top-limit does not work when your limit ends up past the resultset"
390             if $dialect eq 'Top';
391           is ($owners->page(3)->all, 2, "$test_type: has_many prefetch returns correct number of rows");
392           is ($owners->page(3)->count_rs->next, 2, "$test_type: has-many prefetch returns correct count_rs");
393         }
394
395
396         # try a ->belongs_to direction (no select collapse, group_by should work)
397         my $books = $schema->resultset ('BooksInLibrary')->search (
398           {
399             'owner.name' => [qw/wiggle woggle/],
400           },
401           {
402             distinct => 1,
403             having => \['1 = ?', [ test => 1 ] ], #test having propagation
404             prefetch => 'owner',
405             rows     => 2,  # 3 results total
406             order_by => [{ -desc => 'me.owner' }, 'me.id'],
407             unsafe_subselect_ok => 1,
408           },
409         );
410
411         ($sql, @bind) = @${$books->page(3)->as_query};
412         is_same_bind (
413           \@bind,
414           [
415             # inner
416             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
417               => 'wiggle' ],
418             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
419               => 'woggle' ],
420             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' }
421               => 'Library' ],
422             [ { dbic_colname => 'test' }
423               => '1' ],
424
425             # rno(?)
426             $dialect ne 'Top' ? ( [ $OFFSET => 5 ], [ $TOTAL => 6 ] ) : (),
427             # outer
428             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
429               => 'wiggle' ],
430             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
431               => 'woggle' ],
432             [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' }
433               => 'Library' ],
434           ],
435         );
436
437         is ($books->page(1)->all, 2, "$test_type: Prefetched grouped search returns correct number of rows");
438         is ($books->page(1)->count, 2, "$test_type: Prefetched grouped search returns correct count");
439
440         is ($books->page(2)->count, 1, "$test_type: Prefetched grouped search returns correct count");
441         TODO: {
442           local $TODO = "Top-limit does not work when your limit ends up past the resultset"
443             if $dialect eq 'Top';
444           is ($books->page(2)->all, 1, "$test_type: Prefetched grouped search returns correct number of rows");
445           is ($books->page(2)->count_rs->next, 1, "$test_type: Prefetched grouped search returns correct count_rs");
446         }
447       }
448     }
449
450
451 # test GUID columns
452     {
453       $schema->storage->dbh_do (sub {
454         my ($storage, $dbh) = @_;
455         eval { $dbh->do("DROP TABLE artist_guid") };
456         $dbh->do(<<'SQL');
457 CREATE TABLE artist_guid (
458    artistid UNIQUEIDENTIFIER NOT NULL,
459    name VARCHAR(100),
460    rank INT NOT NULL DEFAULT '13',
461    charfield CHAR(10) NULL,
462    a_guid UNIQUEIDENTIFIER,
463    primary key(artistid)
464 )
465 SQL
466       });
467
468       # start disconnected to make sure insert works on an un-reblessed storage
469       $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
470
471       my $row;
472       lives_ok {
473         $row = $schema->resultset('ArtistGUID')->create({ name => 'mtfnpy' })
474       } 'created a row with a GUID';
475
476       ok(
477         eval { $row->artistid },
478         'row has GUID PK col populated',
479       );
480       diag $@ if $@;
481
482       ok(
483         eval { $row->a_guid },
484         'row has a GUID col with auto_nextval populated',
485       );
486       diag $@ if $@;
487
488       my $row_from_db = $schema->resultset('ArtistGUID')
489         ->search({ name => 'mtfnpy' })->first;
490
491       is $row_from_db->artistid, $row->artistid,
492         'PK GUID round trip';
493
494       is $row_from_db->a_guid, $row->a_guid,
495         'NON-PK GUID round trip';
496     }
497
498 # test MONEY type
499     {
500       $schema->storage->dbh_do (sub {
501         my ($storage, $dbh) = @_;
502         eval { $dbh->do("DROP TABLE money_test") };
503         $dbh->do(<<'SQL');
504 CREATE TABLE money_test (
505    id INT IDENTITY PRIMARY KEY,
506    amount MONEY NULL
507 )
508 SQL
509       });
510
511       TODO: {
512         my $freetds_and_dynamic_cursors = 1
513           if $opts_name eq 'use_dynamic_cursors' &&
514             $schema->storage->_using_freetds;
515
516         local $TODO =
517 'these tests fail on freetds with dynamic cursors for some reason'
518           if $freetds_and_dynamic_cursors;
519         local $ENV{DBIC_NULLABLE_KEY_NOWARN} = 1
520           if $freetds_and_dynamic_cursors;
521
522         my $rs = $schema->resultset('Money');
523         my $row;
524
525         lives_ok {
526           $row = $rs->create({ amount => 100 });
527         } 'inserted a money value';
528
529         cmp_ok ((try { $rs->find($row->id)->amount })||0, '==', 100,
530           'money value round-trip');
531
532         lives_ok {
533           $row->update({ amount => 200 });
534         } 'updated a money value';
535
536         cmp_ok ((try { $rs->find($row->id)->amount })||0, '==', 200,
537           'updated money value round-trip');
538
539         lives_ok {
540           $row->update({ amount => undef });
541         } 'updated a money value to NULL';
542
543         is try { $rs->find($row->id)->amount }, undef,
544           'updated money value to NULL round-trip';
545       }
546     }
547   }
548 }
549
550 done_testing;
551
552 # clean up our mess
553 END {
554   if (my $dbh = eval { $schema->storage->_dbh }) {
555     eval { $dbh->do("DROP TABLE $_") }
556       for qw/artist artist_guid money_test books owners/;
557   }
558   undef $schema;
559 }
560 # vim:sw=2 sts=2