Massive rewrite of bind handling, and overall simplification of ::Storage::DBI
[dbsrgits/DBIx-Class.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
e56b1c2d 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
e56b1c2d 83 });
c5f77f6c 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
e56b1c2d 125 {
126 $schema->storage->dbh_do (sub {
127 my ($storage, $dbh) = @_;
128 eval { $dbh->do("DROP TABLE owners") };
129 eval { $dbh->do("DROP TABLE books") };
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
a54bd479 144 });
145
e56b1c2d 146 lives_ok ( sub {
147 # start a new connection, make sure rebless works
148 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
149 $schema->populate ('Owners', [
150 [qw/id name /],
151 [qw/1 wiggle/],
152 [qw/2 woggle/],
153 [qw/3 boggle/],
154 [qw/4 fRIOUX/],
155 [qw/5 fRUE/],
156 [qw/6 fREW/],
157 [qw/7 fROOH/],
158 [qw/8 fISMBoC/],
159 [qw/9 station/],
160 [qw/10 mirror/],
161 [qw/11 dimly/],
162 [qw/12 face_to_face/],
163 [qw/13 icarus/],
164 [qw/14 dream/],
165 [qw/15 dyrstyggyr/],
166 ]);
167 }, 'populate with PKs supplied ok' );
168
169
170 lives_ok (sub {
171 # start a new connection, make sure rebless works
172 # test an insert with a supplied identity, followed by one without
173 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
174 for (2, 1) {
175 my $id = $_ * 20 ;
176 $schema->resultset ('Owners')->create ({ id => $id, name => "troglodoogle $id" });
177 $schema->resultset ('Owners')->create ({ name => "troglodoogle " . ($id + 1) });
178 }
179 }, 'create with/without PKs ok' );
180
181 is ($schema->resultset ('Owners')->count, 19, 'owner rows really in db' );
182
183 lives_ok ( sub {
184 # start a new connection, make sure rebless works
185 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
186 $schema->populate ('BooksInLibrary', [
187 [qw/source owner title /],
188 [qw/Library 1 secrets0/],
189 [qw/Library 1 secrets1/],
190 [qw/Eatery 1 secrets2/],
191 [qw/Library 2 secrets3/],
192 [qw/Library 3 secrets4/],
193 [qw/Eatery 3 secrets5/],
194 [qw/Library 4 secrets6/],
195 [qw/Library 5 secrets7/],
196 [qw/Eatery 5 secrets8/],
197 [qw/Library 6 secrets9/],
198 [qw/Library 7 secrets10/],
199 [qw/Eatery 7 secrets11/],
200 [qw/Library 8 secrets12/],
201 ]);
202 }, 'populate without PKs supplied ok' );
a54bd479 203 }
fb7cd45f 204
e56b1c2d 205# test simple, complex LIMIT and limited prefetch support, with both dialects and quote combinations (if possible)
206 for my $dialect (
207 'Top',
208 ($schema->storage->_server_info->{normalized_dbms_version} || 0 ) >= 9
209 ? ('RowNumberOver')
210 : ()
211 ,
212 ) {
213 for my $quoted (0, 1) {
214
215 $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
216 limit_dialect => $dialect,
217 %$opts,
218 $quoted
219 ? ( quote_char => [ qw/ [ ] / ], name_sep => '.' )
220 : ()
221 ,
222 });
223
224 my $test_type = "Dialect:$dialect Quoted:$quoted";
225
226 # basic limit support
227 TODO: {
228 my $art_rs = $schema->resultset ('Artist');
229 $art_rs->delete;
230 $art_rs->create({ name => 'Artist ' . $_ }) for (1..6);
231
232 my $it = $schema->resultset('Artist')->search( {}, {
233 rows => 4,
234 offset => 3,
235 order_by => 'artistid',
236 });
237
238 is( $it->count, 3, "$test_type: LIMIT count ok" );
239
240 local $TODO = "Top-limit does not work when your limit ends up past the resultset"
241 if $dialect eq 'Top';
242
243 is( $it->next->name, 'Artist 4', "$test_type: iterator->next ok" );
244 $it->next;
245 is( $it->next->name, 'Artist 6', "$test_type: iterator->next ok" );
246 is( $it->next, undef, "$test_type: next past end of resultset ok" );
247 }
f0bd60fc 248
e56b1c2d 249 # plain ordered subqueries throw
250 throws_ok (sub {
251 $schema->resultset('Owners')->search ({}, { order_by => 'name' })->as_query
252 }, qr/ordered subselect encountered/, "$test_type: Ordered Subselect detection throws ok");
a54bd479 253
e56b1c2d 254 # make sure ordered subselects *somewhat* work
255 {
256 my $owners = $schema->resultset ('Owners')->search ({}, { order_by => 'name', offset => 2, rows => 3, unsafe_subselect_ok => 1 });
257 my $sealed_owners = $owners->as_subselect_rs;
258
259 is_deeply (
260 [ map { $_->name } ($sealed_owners->all) ],
261 [ map { $_->name } ($owners->all) ],
262 "$test_type: Sort preserved from within a subquery",
263 );
264 }
a54bd479 265
e56b1c2d 266 {
267 my $book_owner_ids = $schema->resultset ('BooksInLibrary')->search ({}, {
268 rows => 6,
269 offset => 2,
270 join => 'owner',
271 distinct => 1,
272 order_by => 'owner.name',
273 unsafe_subselect_ok => 1
274 })->get_column ('owner');
275
276 my @ids = $book_owner_ids->all;
277
278 is (@ids, 6, 'Limit works');
279
280 my $book_owners = $schema->resultset ('Owners')->search ({
281 id => { -in => $book_owner_ids->as_query }
282 });
283
284 TODO: {
285 local $TODO = "Correlated limited IN subqueries will probably never preserve order";
286
287 is_deeply (
288 [ map { $_->id } ($book_owners->all) ],
289 [ $book_owner_ids->all ],
290 "$test_type: Sort is preserved across IN subqueries",
291 );
292 }
293 }
a54bd479 294
e56b1c2d 295 # still even with lost order of IN, we should be getting correct
296 # sets
297 {
298 my $owners = $schema->resultset ('Owners')->search ({}, { order_by => 'name', offset => 2, rows => 3, unsafe_subselect_ok => 1 });
299 my $corelated_owners = $owners->result_source->resultset->search (
300 {
301 id => { -in => $owners->get_column('id')->as_query },
302 },
303 {
304 order_by => 'name' #reorder because of what is shown above
305 },
306 );
307
308 is (
309 join ("\x00", map { $_->name } ($corelated_owners->all) ),
310 join ("\x00", map { $_->name } ($owners->all) ),
311 "$test_type: With an outer order_by, everything still matches",
312 );
313 }
a54bd479 314
e56b1c2d 315 # make sure right-join-side single-prefetch ordering limit works
316 {
317 my $rs = $schema->resultset ('BooksInLibrary')->search (
318 {
319 'owner.name' => { '!=', 'woggle' },
320 },
321 {
322 prefetch => 'owner',
323 order_by => 'owner.name',
324 }
325 );
326 # this is the order in which they should come from the above query
327 my @owner_names = qw/boggle fISMBoC fREW fRIOUX fROOH fRUE wiggle wiggle/;
328
329 is ($rs->all, 8, "$test_type: Correct amount of objects from right-sorted joined resultset");
330 is_deeply (
331 [map { $_->owner->name } ($rs->all) ],
332 \@owner_names,
333 "$test_type: Prefetched rows were properly ordered"
334 );
335
336 my $limited_rs = $rs->search ({}, {rows => 6, offset => 2, unsafe_subselect_ok => 1});
337 is ($limited_rs->count, 6, "$test_type: Correct count of limited right-sorted joined resultset");
338 is ($limited_rs->count_rs->next, 6, "$test_type: Correct count_rs of limited right-sorted joined resultset");
339
340 my $queries;
341 my $orig_debug = $schema->storage->debug;
342 $schema->storage->debugcb(sub { $queries++; });
343 $schema->storage->debug(1);
344
345 is_deeply (
346 [map { $_->owner->name } ($limited_rs->all) ],
347 [@owner_names[2 .. 7]],
348 "$test_type: Prefetch-limited rows were properly ordered"
349 );
350 is ($queries, 1, "$test_type: Only one query with prefetch");
351
352 $schema->storage->debugcb(undef);
353 $schema->storage->debug($orig_debug);
354
355 is_deeply (
356 [map { $_->name } ($limited_rs->search_related ('owner')->all) ],
357 [@owner_names[2 .. 7]],
358 "$test_type: Rows are still properly ordered after search_related",
359 );
360 }
a54bd479 361
e56b1c2d 362 # try a ->has_many direction with duplicates
363 my $owners = $schema->resultset ('Owners')->search (
364 {
365 'books.id' => { '!=', undef },
366 'me.name' => { '!=', 'somebogusstring' },
367 },
368 {
369 prefetch => 'books',
370 order_by => { -asc => \['name + ?', [ test => 'xxx' ]] }, # test bindvar propagation
371 group_by => [ map { "me.$_" } $schema->source('Owners')->columns ], # the literal order_by requires an explicit group_by
372 rows => 3, # 8 results total
373 unsafe_subselect_ok => 1,
374 },
375 );
a54bd479 376
e56b1c2d 377 my ($sql, @bind) = @${$owners->page(3)->as_query};
0e773352 378 is_same_bind (
e56b1c2d 379 \@bind,
380 [
0e773352 381 $dialect eq 'Top' ? [ { dbic_colname => 'test' } => 'xxx' ] : (), # the extra re-order bind
382 (map {
383 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.name' }
384 => 'somebogusstring' ],
385 [ { dbic_colname => 'test' }
386 => 'xxx' ],
387 } (1,2)), # double because of the prefetch subq
e56b1c2d 388 ],
a54bd479 389 );
fb7cd45f 390
e56b1c2d 391 is ($owners->page(1)->all, 3, "$test_type: has_many prefetch returns correct number of rows");
392 is ($owners->page(1)->count, 3, "$test_type: has-many prefetch returns correct count");
a54bd479 393
e56b1c2d 394 is ($owners->page(3)->count, 2, "$test_type: has-many prefetch returns correct count");
395 TODO: {
396 local $TODO = "Top-limit does not work when your limit ends up past the resultset"
397 if $dialect eq 'Top';
398 is ($owners->page(3)->all, 2, "$test_type: has_many prefetch returns correct number of rows");
399 is ($owners->page(3)->count_rs->next, 2, "$test_type: has-many prefetch returns correct count_rs");
a54bd479 400 }
a54bd479 401
a54bd479 402
e56b1c2d 403 # try a ->belongs_to direction (no select collapse, group_by should work)
404 my $books = $schema->resultset ('BooksInLibrary')->search (
405 {
406 'owner.name' => [qw/wiggle woggle/],
407 },
408 {
409 distinct => 1,
410 having => \['1 = ?', [ test => 1 ] ], #test having propagation
411 prefetch => 'owner',
412 rows => 2, # 3 results total
413 order_by => { -desc => 'me.owner' },
414 unsafe_subselect_ok => 1,
415 },
416 );
6de07ea3 417
e56b1c2d 418 ($sql, @bind) = @${$books->page(3)->as_query};
0e773352 419 is_same_bind (
e56b1c2d 420 \@bind,
421 [
422 # inner
0e773352 423 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
424 => 'wiggle' ],
425 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
426 => 'woggle' ],
427 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' }
428 => 'Library' ],
429 [ { dbic_colname => 'test' }
430 => '1' ],
431
e56b1c2d 432 # outer
0e773352 433 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
434 => 'wiggle' ],
435 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'owner.name' }
436 => 'woggle' ],
437 [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' }
438 => 'Library' ],
e56b1c2d 439 ],
440 );
4bb438ca 441
e56b1c2d 442 is ($books->page(1)->all, 2, "$test_type: Prefetched grouped search returns correct number of rows");
443 is ($books->page(1)->count, 2, "$test_type: Prefetched grouped search returns correct count");
a54bd479 444
e56b1c2d 445 is ($books->page(2)->count, 1, "$test_type: Prefetched grouped search returns correct count");
446 TODO: {
447 local $TODO = "Top-limit does not work when your limit ends up past the resultset"
448 if $dialect eq 'Top';
449 is ($books->page(2)->all, 1, "$test_type: Prefetched grouped search returns correct number of rows");
450 is ($books->page(2)->count_rs->next, 1, "$test_type: Prefetched grouped search returns correct count_rs");
451 }
452 }
a54bd479 453 }
6de07ea3 454
455
a54bd479 456# test GUID columns
e56b1c2d 457 {
458 $schema->storage->dbh_do (sub {
459 my ($storage, $dbh) = @_;
460 eval { $dbh->do("DROP TABLE artist_guid") };
461 $dbh->do(<<'SQL');
b1bdb76d 462CREATE TABLE artist_guid (
a54bd479 463 artistid UNIQUEIDENTIFIER NOT NULL,
464 name VARCHAR(100),
465 rank INT NOT NULL DEFAULT '13',
466 charfield CHAR(10) NULL,
467 a_guid UNIQUEIDENTIFIER,
468 primary key(artistid)
469)
470SQL
e56b1c2d 471 });
8ff60918 472
e56b1c2d 473 # start disconnected to make sure insert works on an un-reblessed storage
474 $schema = DBICTest::Schema->connect($dsn, $user, $pass, $opts);
8ff60918 475
e56b1c2d 476 my $row;
477 lives_ok {
478 $row = $schema->resultset('ArtistGUID')->create({ name => 'mtfnpy' })
479 } 'created a row with a GUID';
8ff60918 480
e56b1c2d 481 ok(
482 eval { $row->artistid },
483 'row has GUID PK col populated',
484 );
485 diag $@ if $@;
8ff60918 486
e56b1c2d 487 ok(
488 eval { $row->a_guid },
489 'row has a GUID col with auto_nextval populated',
490 );
491 diag $@ if $@;
8ff60918 492
e56b1c2d 493 my $row_from_db = $schema->resultset('ArtistGUID')
494 ->search({ name => 'mtfnpy' })->first;
f0bd60fc 495
e56b1c2d 496 is $row_from_db->artistid, $row->artistid,
497 'PK GUID round trip';
b9a2c3a5 498
e56b1c2d 499 is $row_from_db->a_guid, $row->a_guid,
500 'NON-PK GUID round trip';
501 }
02d133f0 502
a54bd479 503# test MONEY type
e56b1c2d 504 {
505 $schema->storage->dbh_do (sub {
506 my ($storage, $dbh) = @_;
507 eval { $dbh->do("DROP TABLE money_test") };
508 $dbh->do(<<'SQL');
a54bd479 509CREATE TABLE money_test (
510 id INT IDENTITY PRIMARY KEY,
511 amount MONEY NULL
512)
513SQL
e56b1c2d 514 });
9010bab8 515
e56b1c2d 516 my $rs = $schema->resultset('Money');
517 my $row;
56d2561e 518
e56b1c2d 519 lives_ok {
520 $row = $rs->create({ amount => 100 });
521 } 'inserted a money value';
6bc666a5 522
e56b1c2d 523 cmp_ok $rs->find($row->id)->amount, '==', 100, 'money value round-trip';
b9a2c3a5 524
e56b1c2d 525 lives_ok {
526 $row->update({ amount => 200 });
527 } 'updated a money value';
fc85215b 528
e56b1c2d 529 cmp_ok $rs->find($row->id)->amount, '==', 200,
530 'updated money value round-trip';
b1e1d073 531
e56b1c2d 532 lives_ok {
533 $row->update({ amount => undef });
534 } 'updated a money value to NULL';
b1e1d073 535
e56b1c2d 536 is $rs->find($row->id)->amount, undef,'updated money value to NULL round-trip';
537 }
538 }
384b8bce 539}
c1cac633 540
afcfff01 541done_testing;
542
c1cac633 543# clean up our mess
544END {
ca791b95 545 if (my $dbh = eval { $schema->storage->_dbh }) {
546 eval { $dbh->do("DROP TABLE $_") }
b1bdb76d 547 for qw/artist artist_guid money_test books owners/;
ca791b95 548 }
c1cac633 549}
fc85215b 550# vim:sw=2 sts=2