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