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