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