More oracle sequence detection woes RT#63493
[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
7137528d 311=head2 get_autoinc_seq
312
313Returns the sequence name for an autoincrement column
314
315=cut
316
18360aed 317sub get_autoinc_seq {
318 my ($self, $source, $col) = @_;
d4daee7b 319
373940e1 320 $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
18360aed 321}
322
8f7e044c 323=head2 datetime_parser_type
324
325This sets the proper DateTime::Format module for use with
326L<DBIx::Class::InflateColumn::DateTime>.
327
328=cut
329
330sub datetime_parser_type { return "DateTime::Format::Oracle"; }
331
9900b569 332=head2 connect_call_datetime_setup
d2a3958e 333
334Used as:
335
9900b569 336 on_connect_call => 'datetime_setup'
d2a3958e 337
8384a713 338In L<connect_info|DBIx::Class::Storage::DBI/connect_info> to set the session nls
339date, and timestamp values for use with L<DBIx::Class::InflateColumn::DateTime>
340and the necessary environment variables for L<DateTime::Format::Oracle>, which
341is used by it.
d2a3958e 342
82f6f45f 343Maximum allowable precision is used, unless the environment variables have
344already been set.
d2a3958e 345
9900b569 346These are the defaults used:
347
348 $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
349 $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF';
350 $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
351
d9e53b85 352To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
353for your timestamps, use something like this:
354
355 use Time::HiRes 'time';
356 my $ts = DateTime->from_epoch(epoch => time);
357
d2a3958e 358=cut
359
9900b569 360sub connect_call_datetime_setup {
d2a3958e 361 my $self = shift;
d2a3958e 362
363 my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
364 my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
365 'YYYY-MM-DD HH24:MI:SS.FF';
366 my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
367 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
368
7a84c41b 369 $self->_do_query(
d7a58a29 370 "alter session set nls_date_format = '$date_format'"
371 );
7a84c41b 372 $self->_do_query(
d7a58a29 373 "alter session set nls_timestamp_format = '$timestamp_format'"
374 );
7a84c41b 375 $self->_do_query(
d7a58a29 376 "alter session set nls_timestamp_tz_format='$timestamp_tz_format'"
377 );
d2a3958e 378}
379
5db2758d 380=head2 source_bind_attributes
381
382Handle LOB types in Oracle. Under a certain size (4k?), you can get away
383with the driver assuming your input is the deprecated LONG type if you
384encode it as a hex string. That ain't gonna fly at larger values, where
385you'll discover you have to do what this does.
386
387This method had to be overridden because we need to set ora_field to the
388actual column, and that isn't passed to the call (provided by Storage) to
389bind_attribute_by_data_type.
390
391According to L<DBD::Oracle>, the ora_field isn't always necessary, but
392adding it doesn't hurt, and will save your bacon if you're modifying a
393table with more than one LOB column.
394
395=cut
396
e6dd7b42 397sub source_bind_attributes
5db2758d 398{
d7a58a29 399 require DBD::Oracle;
400 my $self = shift;
401 my($source) = @_;
5db2758d 402
d7a58a29 403 my %bind_attributes;
5db2758d 404
d7a58a29 405 foreach my $column ($source->columns) {
07cda1c5 406 my $data_type = $source->column_info($column)->{data_type}
407 or next;
5db2758d 408
d7a58a29 409 my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type);
5db2758d 410
d7a58a29 411 if ($data_type =~ /^[BC]LOB$/i) {
931e5d43 412 if ($DBD::Oracle::VERSION eq '1.23') {
413 $self->throw_exception(
414"BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
555cc3f4 415"version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
931e5d43 416 );
417 }
5db2758d 418
d7a58a29 419 $column_bind_attrs{'ora_type'} = uc($data_type) eq 'CLOB'
420 ? DBD::Oracle::ORA_CLOB()
421 : DBD::Oracle::ORA_BLOB()
422 ;
423 $column_bind_attrs{'ora_field'} = $column;
424 }
5db2758d 425
d7a58a29 426 $bind_attributes{$column} = \%column_bind_attrs;
427 }
5db2758d 428
d7a58a29 429 return \%bind_attributes;
5db2758d 430}
431
1816be4f 432sub _svp_begin {
d7a58a29 433 my ($self, $name) = @_;
434 $self->_get_dbh->do("SAVEPOINT $name");
1816be4f 435}
436
281719d2 437# Oracle automatically releases a savepoint when you start another one with the
438# same name.
439sub _svp_release { 1 }
440
441sub _svp_rollback {
d7a58a29 442 my ($self, $name) = @_;
443 $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name")
281719d2 444}
445
6c0230de 446=head2 relname_to_table_alias
447
448L<DBIx::Class> uses L<DBIx::Class::Relationship> names as table aliases in
449queries.
450
451Unfortunately, Oracle doesn't support identifiers over 30 chars in length, so
af0edca1 452the L<DBIx::Class::Relationship> name is shortened and appended with half of an
453MD5 hash.
6c0230de 454
455See L<DBIx::Class::Storage/"relname_to_table_alias">.
456
457=cut
458
459sub relname_to_table_alias {
460 my $self = shift;
461 my ($relname, $join_count) = @_;
462
463 my $alias = $self->next::method(@_);
464
63ca94e1 465 return $self->sql_maker->_shorten_identifier($alias, [$relname]);
6c0230de 466}
467
6c0bb6a7 468=head2 with_deferred_fk_checks
469
470Runs a coderef between:
471
472 alter session set constraints = deferred
473 ...
474 alter session set constraints = immediate
475
b7b18f32 476to defer foreign key checks.
477
478Constraints must be declared C<DEFERRABLE> for this to work.
6c0bb6a7 479
480=cut
481
482sub with_deferred_fk_checks {
483 my ($self, $sub) = @_;
b7b18f32 484
485 my $txn_scope_guard = $self->txn_scope_guard;
486
6c0bb6a7 487 $self->_do_query('alter session set constraints = deferred');
54161a15 488
b7b18f32 489 my $sg = Scope::Guard->new(sub {
490 $self->_do_query('alter session set constraints = immediate');
491 });
281719d2 492
6298a324 493 return
494 preserve_context { $sub->() } after => sub { $txn_scope_guard->commit };
281719d2 495}
496
c0024355 497=head1 ATTRIBUTES
498
499Following additional attributes can be used in resultsets.
500
6b2fbbf0 501=head2 connect_by or connect_by_nocycle
c0024355 502
503=over 4
504
505=item Value: \%connect_by
506
507=back
508
509A hashref of conditions used to specify the relationship between parent rows
510and child rows of the hierarchy.
511
6b2fbbf0 512
c0024355 513 connect_by => { parentid => 'prior personid' }
514
515 # adds a connect by statement to the query:
516 # SELECT
517 # me.persionid me.firstname, me.lastname, me.parentid
518 # FROM
519 # person me
520 # CONNECT BY
521 # parentid = prior persionid
6b2fbbf0 522
c0024355 523
6b2fbbf0 524 connect_by_nocycle => { parentid => 'prior personid' }
2ba03b16 525
6b2fbbf0 526 # adds a connect by statement to the query:
527 # SELECT
528 # me.persionid me.firstname, me.lastname, me.parentid
529 # FROM
530 # person me
531 # CONNECT BY NOCYCLE
532 # parentid = prior persionid
2ba03b16 533
534
c0024355 535=head2 start_with
536
537=over 4
538
539=item Value: \%condition
540
541=back
542
543A hashref of conditions which specify the root row(s) of the hierarchy.
544
545It uses the same syntax as L<DBIx::Class::ResultSet/search>
546
547 start_with => { firstname => 'Foo', lastname => 'Bar' }
548
549 # SELECT
550 # me.persionid me.firstname, me.lastname, me.parentid
551 # FROM
552 # person me
553 # START WITH
554 # firstname = 'foo' and lastname = 'bar'
555 # CONNECT BY
556 # parentid = prior persionid
557
558=head2 order_siblings_by
559
560=over 4
561
562=item Value: ($order_siblings_by | \@order_siblings_by)
563
564=back
565
566Which column(s) to order the siblings by.
567
568It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
569
570 'order_siblings_by' => 'firstname ASC'
571
572 # SELECT
573 # me.persionid me.firstname, me.lastname, me.parentid
574 # FROM
575 # person me
576 # CONNECT BY
577 # parentid = prior persionid
578 # ORDER SIBLINGS BY
579 # firstname ASC
580
7a84c41b 581=head1 AUTHOR
18360aed 582
7a84c41b 583See L<DBIx::Class/CONTRIBUTORS>.
18360aed 584
585=head1 LICENSE
586
587You may distribute this code under the same terms as Perl itself.
588
589=cut
7137528d 590
5911;