Massive rewrite of bind handling, and overall simplification of ::Storage::DBI
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Oracle / Generic.pm
CommitLineData
18360aed 1package DBIx::Class::Storage::DBI::Oracle::Generic;
2
3use strict;
4use warnings;
b7b18f32 5use Scope::Guard ();
6298a324 6use Context::Preserve 'preserve_context';
ed7ab0f4 7use Try::Tiny;
fd323bf1 8use namespace::clean;
18360aed 9
6a247f33 10__PACKAGE__->sql_limit_dialect ('RowNum');
2b8cc2f2 11__PACKAGE__->sql_quote_char ('"');
6a247f33 12
7137528d 13=head1 NAME
14
7a84c41b 15DBIx::Class::Storage::DBI::Oracle::Generic - Oracle Support for DBIx::Class
7137528d 16
17=head1 SYNOPSIS
18
d88ecca6 19 # In your result (table) classes
20 use base 'DBIx::Class::Core';
2e46b6eb 21 __PACKAGE__->add_columns({ id => { sequence => 'mysequence', auto_nextval => 1 } });
7137528d 22 __PACKAGE__->set_primary_key('id');
7137528d 23
c0024355 24 # Somewhere in your Code
25 # add some data to a table with a hierarchical relationship
26 $schema->resultset('Person')->create ({
27 firstname => 'foo',
28 lastname => 'bar',
29 children => [
30 {
31 firstname => 'child1',
32 lastname => 'bar',
33 children => [
34 {
35 firstname => 'grandchild',
36 lastname => 'bar',
37 }
38 ],
39 },
40 {
41 firstname => 'child2',
42 lastname => 'bar',
43 },
44 ],
45 });
46
47 # select from the hierarchical relationship
48 my $rs = $schema->resultset('Person')->search({},
49 {
50 'start_with' => { 'firstname' => 'foo', 'lastname' => 'bar' },
e6600283 51 'connect_by' => { 'parentid' => { '-prior' => { -ident => 'personid' } },
25ca709b 52 'order_siblings_by' => { -asc => 'name' },
c0024355 53 };
54 );
55
56 # this will select the whole tree starting from person "foo bar", creating
57 # following query:
58 # SELECT
59 # me.persionid me.firstname, me.lastname, me.parentid
60 # FROM
61 # person me
62 # START WITH
63 # firstname = 'foo' and lastname = 'bar'
64 # CONNECT BY
e6600283 65 # parentid = prior personid
c0024355 66 # ORDER SIBLINGS BY
67 # firstname ASC
68
7137528d 69=head1 DESCRIPTION
70
6c0230de 71This class implements base Oracle support. The subclass
72L<DBIx::Class::Storage::DBI::Oracle::WhereJoins> is for C<(+)> joins in Oracle
73versions before 9.
7137528d 74
75=head1 METHODS
76
77=cut
78
db56cf3d 79use base qw/DBIx::Class::Storage::DBI/;
2ad62d97 80use mro 'c3';
18360aed 81
d5dedbd6 82__PACKAGE__->sql_maker_class('DBIx::Class::SQLMaker::Oracle');
6f7a118e 83__PACKAGE__->datetime_parser_type('DateTime::Format::Oracle');
c0024355 84
bf51641f 85sub _determine_supports_insert_returning {
86 my $self = shift;
87
88# TODO find out which version supports the RETURNING syntax
89# 8i has it and earlier docs are a 404 on oracle.com
90
91 return 1
92 if $self->_server_info->{normalized_dbms_version} >= 8.001;
93
94 return 0;
95}
96
97__PACKAGE__->_use_insert_returning_bound (1);
98
dd2600c6 99sub deployment_statements {
100 my $self = shift;;
101 my ($schema, $type, $version, $dir, $sqltargs, @rest) = @_;
102
103 $sqltargs ||= {};
032b2366 104 my $quote_char = $self->schema->storage->sql_maker->quote_char;
105 $sqltargs->{quote_table_names} = $quote_char ? 1 : 0;
106 $sqltargs->{quote_field_names} = $quote_char ? 1 : 0;
dd2600c6 107
96736321 108 if (
109 ! exists $sqltargs->{producer_args}{oracle_version}
110 and
111 my $dver = $self->_server_info->{dbms_version}
112 ) {
113 $sqltargs->{producer_args}{oracle_version} = $dver;
114 }
a4433d8e 115
38aead8e 116 $self->next::method($schema, $type, $version, $dir, $sqltargs, @rest);
dd2600c6 117}
118
18360aed 119sub _dbh_last_insert_id {
2e46b6eb 120 my ($self, $dbh, $source, @columns) = @_;
121 my @ids = ();
122 foreach my $col (@columns) {
123 my $seq = ($source->column_info($col)->{sequence} ||= $self->get_autoinc_seq($source,$col));
07cda1c5 124 my $id = $self->_sequence_fetch( 'CURRVAL', $seq );
2e46b6eb 125 push @ids, $id;
126 }
127 return @ids;
18360aed 128}
129
130sub _dbh_get_autoinc_seq {
131 my ($self, $dbh, $source, $col) = @_;
132
032b2366 133 my $sql_maker = $self->sql_maker;
07cda1c5 134 my ($ql, $qr) = map { $_ ? (quotemeta $_) : '' } $sql_maker->_quote_chars;
cb464582 135
e6dd7b42 136 my $source_name;
032b2366 137 if ( ref $source->name eq 'SCALAR' ) {
138 $source_name = ${$source->name};
07cda1c5 139
140 # the ALL_TRIGGERS match further on is case sensitive - thus uppercase
141 # stuff unless it is already quoted
142 $source_name = uc ($source_name) if $source_name !~ /\"/;
e6dd7b42 143 }
144 else {
032b2366 145 $source_name = $source->name;
07cda1c5 146 $source_name = uc($source_name) unless $ql;
e6dd7b42 147 }
38aead8e 148
032b2366 149 # trigger_body is a LONG
150 local $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024);
151
152 # disable default bindtype
153 local $sql_maker->{bindtype} = 'normal';
154
155 # look up the correct sequence automatically
07cda1c5 156 my ( $schema, $table ) = $source_name =~ /( (?:${ql})? \w+ (?:${qr})? ) \. ( (?:${ql})? \w+ (?:${qr})? )/x;
a6646e1b 157
158 # if no explicit schema was requested - use the default schema (which in the case of Oracle is the db user)
159 $schema ||= uc( ($self->_dbi_connect_info||[])->[1] || '');
160
032b2366 161 my ($sql, @bind) = $sql_maker->select (
162 'ALL_TRIGGERS',
07cda1c5 163 [qw/TRIGGER_BODY TABLE_OWNER TRIGGER_NAME/],
032b2366 164 {
07cda1c5 165 $schema ? (OWNER => $schema) : (),
166 TABLE_NAME => $table || $source_name,
167 TRIGGERING_EVENT => { -like => '%INSERT%' }, # this will also catch insert_or_update
168 TRIGGER_TYPE => { -like => '%BEFORE%' }, # we care only about 'before' triggers
169 STATUS => 'ENABLED',
032b2366 170 },
171 );
e6dd7b42 172
6f5f880d 173 # to find all the triggers that mention the column in question a simple
174 # regex grep since the trigger_body above is a LONG and hence not searchable
175 my @triggers = ( map
176 { my %inf; @inf{qw/body schema name/} = @$_; \%inf }
177 ( grep
07cda1c5 178 { $_->[0] =~ /\:new\.${ql}${col}${qr} | \:new\.$col/xi }
6f5f880d 179 @{ $dbh->selectall_arrayref( $sql, {}, @bind ) }
180 )
181 );
182
183 # extract all sequence names mentioned in each trigger
184 for (@triggers) {
185 $_->{sequences} = [ $_->{body} =~ / ( "? [\.\w\"\-]+ "? ) \. nextval /xig ];
186 }
187
188 my $chosen_trigger;
189
190 # if only one trigger matched things are easy
191 if (@triggers == 1) {
192
193 if ( @{$triggers[0]{sequences}} == 1 ) {
194 $chosen_trigger = $triggers[0];
195 }
196 else {
197 $self->throw_exception( sprintf (
198 "Unable to introspect trigger '%s' for column %s.%s (references multiple sequences). "
199 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
200 $triggers[0]{name},
201 $source_name,
202 $col,
203 $col,
204 ) );
205 }
206 }
207 # got more than one matching trigger - see if we can narrow it down
208 elsif (@triggers > 1) {
df6e3f5c 209
6f5f880d 210 my @candidates = grep
211 { $_->{body} =~ / into \s+ \:new\.$col /xi }
212 @triggers
213 ;
df6e3f5c 214
6f5f880d 215 if (@candidates == 1 && @{$candidates[0]{sequences}} == 1) {
216 $chosen_trigger = $candidates[0];
df6e3f5c 217 }
6f5f880d 218 else {
219 $self->throw_exception( sprintf (
220 "Unable to reliably select a BEFORE INSERT trigger for column %s.%s (possibilities: %s). "
221 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
222 $source_name,
223 $col,
224 ( join ', ', map { "'$_->{name}'" } @triggers ),
225 $col,
226 ) );
227 }
228 }
229
230 if ($chosen_trigger) {
231 my $seq_name = $chosen_trigger->{sequences}[0];
232
233 $seq_name = "$chosen_trigger->{schema}.$seq_name"
234 unless $seq_name =~ /\./;
df6e3f5c 235
07cda1c5 236 return \$seq_name if $seq_name =~ /\"/; # may already be quoted in-trigger
df6e3f5c 237 return $seq_name;
18360aed 238 }
6f5f880d 239
240 $self->throw_exception( sprintf (
241 "No suitable BEFORE INSERT triggers found for column %s.%s. "
242 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
243 $source_name,
244 $col,
245 $col,
246 ));
18360aed 247}
248
2e46b6eb 249sub _sequence_fetch {
250 my ( $self, $type, $seq ) = @_;
07cda1c5 251
252 # use the maker to leverage quoting settings
253 my $sql_maker = $self->sql_maker;
254 my ($id) = $self->_get_dbh->selectrow_array ($sql_maker->select('DUAL', [ ref $seq ? \"$$seq.$type" : "$seq.$type" ] ) );
2e46b6eb 255 return $id;
256}
257
6dc4be0f 258sub _ping {
c2481821 259 my $self = shift;
7ba7a57d 260
6dc4be0f 261 my $dbh = $self->_dbh or return 0;
7ba7a57d 262
6dc4be0f 263 local $dbh->{RaiseError} = 1;
ecdf1ac8 264 local $dbh->{PrintError} = 0;
c2d7baef 265
52b420dd 266 return try {
ecdf1ac8 267 $dbh->do('select 1 from dual');
52b420dd 268 1;
ed7ab0f4 269 } catch {
52b420dd 270 0;
6dc4be0f 271 };
c2481821 272}
273
d789fa99 274sub _dbh_execute {
0e773352 275 my ($self, $dbh, $sql, @args) = @_;
d789fa99 276
87560ef9 277 my (@res, $tried);
cca282b6 278 my $want = wantarray;
4f661051 279 my $next = $self->next::can;
87560ef9 280 do {
52b420dd 281 try {
0e773352 282 my $exec = sub { $self->$next($dbh, $sql, @args) };
dd415de8 283
cca282b6 284 if (!defined $want) {
dd415de8 285 $exec->();
286 }
cca282b6 287 elsif (! $want) {
dd415de8 288 $res[0] = $exec->();
289 }
290 else {
291 @res = $exec->();
292 }
87560ef9 293
294 $tried++;
52b420dd 295 }
296 catch {
87560ef9 297 if (! $tried and $_ =~ /ORA-01003/) {
0f0abc97 298 # ORA-01003: no statement parsed (someone changed the table somehow,
299 # invalidating your cursor.)
0f0abc97 300 delete $dbh->{CachedKids}{$sql};
d789fa99 301 }
52b420dd 302 else {
303 $self->throw_exception($_);
304 }
305 };
87560ef9 306 } while (! $tried++);
d789fa99 307
cca282b6 308 return wantarray ? @res : $res[0];
d789fa99 309}
310
a5a27e7a 311sub _dbh_execute_array {
312 #my ($self, $sth, $tuple_status, @extra) = @_;
313
314 # DBD::Oracle warns loudly on partial execute_array failures
315 local $_[1]->{PrintWarn} = 0;
316
317 shift->next::method(@_);
318}
319
7137528d 320=head2 get_autoinc_seq
321
322Returns the sequence name for an autoincrement column
323
324=cut
325
18360aed 326sub get_autoinc_seq {
327 my ($self, $source, $col) = @_;
d4daee7b 328
373940e1 329 $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
18360aed 330}
331
8f7e044c 332=head2 datetime_parser_type
333
334This sets the proper DateTime::Format module for use with
335L<DBIx::Class::InflateColumn::DateTime>.
336
9900b569 337=head2 connect_call_datetime_setup
d2a3958e 338
339Used as:
340
9900b569 341 on_connect_call => 'datetime_setup'
d2a3958e 342
8384a713 343In L<connect_info|DBIx::Class::Storage::DBI/connect_info> to set the session nls
344date, and timestamp values for use with L<DBIx::Class::InflateColumn::DateTime>
345and the necessary environment variables for L<DateTime::Format::Oracle>, which
346is used by it.
d2a3958e 347
82f6f45f 348Maximum allowable precision is used, unless the environment variables have
349already been set.
d2a3958e 350
9900b569 351These are the defaults used:
352
353 $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
354 $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF';
355 $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
356
d9e53b85 357To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
358for your timestamps, use something like this:
359
360 use Time::HiRes 'time';
361 my $ts = DateTime->from_epoch(epoch => time);
362
d2a3958e 363=cut
364
9900b569 365sub connect_call_datetime_setup {
d2a3958e 366 my $self = shift;
d2a3958e 367
368 my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
369 my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
370 'YYYY-MM-DD HH24:MI:SS.FF';
371 my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
372 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
373
7a84c41b 374 $self->_do_query(
d7a58a29 375 "alter session set nls_date_format = '$date_format'"
376 );
7a84c41b 377 $self->_do_query(
d7a58a29 378 "alter session set nls_timestamp_format = '$timestamp_format'"
379 );
7a84c41b 380 $self->_do_query(
d7a58a29 381 "alter session set nls_timestamp_tz_format='$timestamp_tz_format'"
382 );
d2a3958e 383}
384
0e773352 385### Note originally by Ron "Quinn" Straight <quinnfazigu@gmail.org>
386### http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/DBIx-Class.git;a=commitdiff;h=5db2758de644d53e07cd3e05f0e9037bf40116fc
387#
388# Handle LOB types in Oracle. Under a certain size (4k?), you can get away
389# with the driver assuming your input is the deprecated LONG type if you
390# encode it as a hex string. That ain't gonna fly at larger values, where
391# you'll discover you have to do what this does.
392#
393# This method had to be overridden because we need to set ora_field to the
394# actual column, and that isn't passed to the call (provided by Storage) to
395# bind_attribute_by_data_type.
396#
397# According to L<DBD::Oracle>, the ora_field isn't always necessary, but
398# adding it doesn't hurt, and will save your bacon if you're modifying a
399# table with more than one LOB column.
400#
401sub _dbi_attrs_for_bind {
402 my ($self, $ident, $bind) = @_;
403 my $attrs = $self->next::method($ident, $bind);
404
405 for my $i (0 .. $#$attrs) {
406 if (keys %{$attrs->[$i]||{}} and my $col = $bind->[$i][0]{dbic_colname}) {
407 $attrs->[$i]{ora_field} = $col;
408 }
409 }
5db2758d 410
0e773352 411 $attrs;
412}
5db2758d 413
0e773352 414my $dbd_loaded;
415sub bind_attribute_by_data_type {
416 my ($self, $dt) = @_;
417
418 $dbd_loaded ||= do {
419 require DBD::Oracle;
420 if ($DBD::Oracle::VERSION eq '1.23') {
421 $self->throw_exception(
422 "BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
423 "version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
424 );
425 }
426 1;
427 };
5db2758d 428
0e773352 429 if ($self->_is_lob_type($dt)) {
430 return {
431 ora_type => $self->_is_text_lob_type($dt)
d7a58a29 432 ? DBD::Oracle::ORA_CLOB()
433 : DBD::Oracle::ORA_BLOB()
0e773352 434 };
d7a58a29 435 }
5db2758d 436}
437
1816be4f 438sub _svp_begin {
d7a58a29 439 my ($self, $name) = @_;
440 $self->_get_dbh->do("SAVEPOINT $name");
1816be4f 441}
442
281719d2 443# Oracle automatically releases a savepoint when you start another one with the
444# same name.
445sub _svp_release { 1 }
446
447sub _svp_rollback {
d7a58a29 448 my ($self, $name) = @_;
449 $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name")
281719d2 450}
451
6c0230de 452=head2 relname_to_table_alias
453
454L<DBIx::Class> uses L<DBIx::Class::Relationship> names as table aliases in
455queries.
456
457Unfortunately, Oracle doesn't support identifiers over 30 chars in length, so
af0edca1 458the L<DBIx::Class::Relationship> name is shortened and appended with half of an
459MD5 hash.
6c0230de 460
461See L<DBIx::Class::Storage/"relname_to_table_alias">.
462
463=cut
464
465sub relname_to_table_alias {
466 my $self = shift;
467 my ($relname, $join_count) = @_;
468
469 my $alias = $self->next::method(@_);
470
19c4cc62 471 # we need to shorten here in addition to the shortening in SQLA itself,
472 # since the final relnames are a crucial for the join optimizer
473 return $self->sql_maker->_shorten_identifier($alias);
6c0230de 474}
475
6c0bb6a7 476=head2 with_deferred_fk_checks
477
478Runs a coderef between:
479
480 alter session set constraints = deferred
481 ...
482 alter session set constraints = immediate
483
b7b18f32 484to defer foreign key checks.
485
486Constraints must be declared C<DEFERRABLE> for this to work.
6c0bb6a7 487
488=cut
489
490sub with_deferred_fk_checks {
491 my ($self, $sub) = @_;
b7b18f32 492
493 my $txn_scope_guard = $self->txn_scope_guard;
494
6c0bb6a7 495 $self->_do_query('alter session set constraints = deferred');
54161a15 496
b7b18f32 497 my $sg = Scope::Guard->new(sub {
498 $self->_do_query('alter session set constraints = immediate');
499 });
281719d2 500
6298a324 501 return
502 preserve_context { $sub->() } after => sub { $txn_scope_guard->commit };
281719d2 503}
504
c0024355 505=head1 ATTRIBUTES
506
507Following additional attributes can be used in resultsets.
508
6b2fbbf0 509=head2 connect_by or connect_by_nocycle
c0024355 510
511=over 4
512
513=item Value: \%connect_by
514
515=back
516
517A hashref of conditions used to specify the relationship between parent rows
518and child rows of the hierarchy.
519
6b2fbbf0 520
c0024355 521 connect_by => { parentid => 'prior personid' }
522
523 # adds a connect by statement to the query:
524 # SELECT
525 # me.persionid me.firstname, me.lastname, me.parentid
526 # FROM
527 # person me
528 # CONNECT BY
529 # parentid = prior persionid
6b2fbbf0 530
c0024355 531
6b2fbbf0 532 connect_by_nocycle => { parentid => 'prior personid' }
2ba03b16 533
6b2fbbf0 534 # adds a connect by statement to the query:
535 # SELECT
536 # me.persionid me.firstname, me.lastname, me.parentid
537 # FROM
538 # person me
539 # CONNECT BY NOCYCLE
540 # parentid = prior persionid
2ba03b16 541
542
c0024355 543=head2 start_with
544
545=over 4
546
547=item Value: \%condition
548
549=back
550
551A hashref of conditions which specify the root row(s) of the hierarchy.
552
553It uses the same syntax as L<DBIx::Class::ResultSet/search>
554
555 start_with => { firstname => 'Foo', lastname => 'Bar' }
556
557 # SELECT
558 # me.persionid me.firstname, me.lastname, me.parentid
559 # FROM
560 # person me
561 # START WITH
562 # firstname = 'foo' and lastname = 'bar'
563 # CONNECT BY
564 # parentid = prior persionid
565
566=head2 order_siblings_by
567
568=over 4
569
570=item Value: ($order_siblings_by | \@order_siblings_by)
571
572=back
573
574Which column(s) to order the siblings by.
575
576It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
577
578 'order_siblings_by' => 'firstname ASC'
579
580 # SELECT
581 # me.persionid me.firstname, me.lastname, me.parentid
582 # FROM
583 # person me
584 # CONNECT BY
585 # parentid = prior persionid
586 # ORDER SIBLINGS BY
587 # firstname ASC
588
7a84c41b 589=head1 AUTHOR
18360aed 590
7a84c41b 591See L<DBIx::Class/CONTRIBUTORS>.
18360aed 592
593=head1 LICENSE
594
595You may distribute this code under the same terms as Perl itself.
596
597=cut
7137528d 598
5991;