Privatize _sth
[dbsrgits/DBIx-Class-Historic.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 {
275 my $self = shift;
276 my ($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_;
277
87560ef9 278 my (@res, $tried);
cca282b6 279 my $want = wantarray;
4f661051 280 my $next = $self->next::can;
87560ef9 281 do {
52b420dd 282 try {
dd415de8 283 my $exec = sub { $self->$next($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) };
284
cca282b6 285 if (!defined $want) {
dd415de8 286 $exec->();
287 }
cca282b6 288 elsif (! $want) {
dd415de8 289 $res[0] = $exec->();
290 }
291 else {
292 @res = $exec->();
293 }
87560ef9 294
295 $tried++;
52b420dd 296 }
297 catch {
87560ef9 298 if (! $tried and $_ =~ /ORA-01003/) {
0f0abc97 299 # ORA-01003: no statement parsed (someone changed the table somehow,
300 # invalidating your cursor.)
301 my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args);
302 delete $dbh->{CachedKids}{$sql};
d789fa99 303 }
52b420dd 304 else {
305 $self->throw_exception($_);
306 }
307 };
87560ef9 308 } while (! $tried++);
d789fa99 309
cca282b6 310 return wantarray ? @res : $res[0];
d789fa99 311}
312
a5a27e7a 313sub _dbh_execute_array {
314 #my ($self, $sth, $tuple_status, @extra) = @_;
315
316 # DBD::Oracle warns loudly on partial execute_array failures
317 local $_[1]->{PrintWarn} = 0;
318
319 shift->next::method(@_);
320}
321
7137528d 322=head2 get_autoinc_seq
323
324Returns the sequence name for an autoincrement column
325
326=cut
327
18360aed 328sub get_autoinc_seq {
329 my ($self, $source, $col) = @_;
d4daee7b 330
373940e1 331 $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
18360aed 332}
333
8f7e044c 334=head2 datetime_parser_type
335
336This sets the proper DateTime::Format module for use with
337L<DBIx::Class::InflateColumn::DateTime>.
338
9900b569 339=head2 connect_call_datetime_setup
d2a3958e 340
341Used as:
342
9900b569 343 on_connect_call => 'datetime_setup'
d2a3958e 344
8384a713 345In L<connect_info|DBIx::Class::Storage::DBI/connect_info> to set the session nls
346date, and timestamp values for use with L<DBIx::Class::InflateColumn::DateTime>
347and the necessary environment variables for L<DateTime::Format::Oracle>, which
348is used by it.
d2a3958e 349
82f6f45f 350Maximum allowable precision is used, unless the environment variables have
351already been set.
d2a3958e 352
9900b569 353These are the defaults used:
354
355 $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
356 $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF';
357 $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
358
d9e53b85 359To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
360for your timestamps, use something like this:
361
362 use Time::HiRes 'time';
363 my $ts = DateTime->from_epoch(epoch => time);
364
d2a3958e 365=cut
366
9900b569 367sub connect_call_datetime_setup {
d2a3958e 368 my $self = shift;
d2a3958e 369
370 my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
371 my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
372 'YYYY-MM-DD HH24:MI:SS.FF';
373 my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
374 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
375
7a84c41b 376 $self->_do_query(
d7a58a29 377 "alter session set nls_date_format = '$date_format'"
378 );
7a84c41b 379 $self->_do_query(
d7a58a29 380 "alter session set nls_timestamp_format = '$timestamp_format'"
381 );
7a84c41b 382 $self->_do_query(
d7a58a29 383 "alter session set nls_timestamp_tz_format='$timestamp_tz_format'"
384 );
d2a3958e 385}
386
5db2758d 387=head2 source_bind_attributes
388
389Handle LOB types in Oracle. Under a certain size (4k?), you can get away
390with the driver assuming your input is the deprecated LONG type if you
391encode it as a hex string. That ain't gonna fly at larger values, where
392you'll discover you have to do what this does.
393
394This method had to be overridden because we need to set ora_field to the
395actual column, and that isn't passed to the call (provided by Storage) to
396bind_attribute_by_data_type.
397
398According to L<DBD::Oracle>, the ora_field isn't always necessary, but
399adding it doesn't hurt, and will save your bacon if you're modifying a
400table with more than one LOB column.
401
402=cut
403
e6dd7b42 404sub source_bind_attributes
5db2758d 405{
d7a58a29 406 require DBD::Oracle;
407 my $self = shift;
408 my($source) = @_;
5db2758d 409
8138f96a 410 my %bind_attributes = %{ $self->next::method(@_) };
5db2758d 411
d7a58a29 412 foreach my $column ($source->columns) {
8138f96a 413 my %column_bind_attrs = %{ $bind_attributes{$column} || {} };
5db2758d 414
8138f96a 415 my $data_type = $source->column_info($column)->{data_type};
5db2758d 416
8138f96a 417 if ($self->_is_lob_type($data_type)) {
931e5d43 418 if ($DBD::Oracle::VERSION eq '1.23') {
419 $self->throw_exception(
420"BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
555cc3f4 421"version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
931e5d43 422 );
423 }
5db2758d 424
8138f96a 425 $column_bind_attrs{'ora_type'} = $self->_is_text_lob_type($data_type)
d7a58a29 426 ? DBD::Oracle::ORA_CLOB()
427 : DBD::Oracle::ORA_BLOB()
428 ;
429 $column_bind_attrs{'ora_field'} = $column;
430 }
5db2758d 431
d7a58a29 432 $bind_attributes{$column} = \%column_bind_attrs;
433 }
5db2758d 434
d7a58a29 435 return \%bind_attributes;
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;