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