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