More robust Oracle autoinc pk sequence detection
[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');
22 __PACKAGE__->sequence('mysequence');
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');
c0024355 83
dd2600c6 84sub deployment_statements {
85 my $self = shift;;
86 my ($schema, $type, $version, $dir, $sqltargs, @rest) = @_;
87
88 $sqltargs ||= {};
032b2366 89 my $quote_char = $self->schema->storage->sql_maker->quote_char;
90 $sqltargs->{quote_table_names} = $quote_char ? 1 : 0;
91 $sqltargs->{quote_field_names} = $quote_char ? 1 : 0;
dd2600c6 92
96736321 93 if (
94 ! exists $sqltargs->{producer_args}{oracle_version}
95 and
96 my $dver = $self->_server_info->{dbms_version}
97 ) {
98 $sqltargs->{producer_args}{oracle_version} = $dver;
99 }
a4433d8e 100
38aead8e 101 $self->next::method($schema, $type, $version, $dir, $sqltargs, @rest);
dd2600c6 102}
103
18360aed 104sub _dbh_last_insert_id {
2e46b6eb 105 my ($self, $dbh, $source, @columns) = @_;
106 my @ids = ();
107 foreach my $col (@columns) {
108 my $seq = ($source->column_info($col)->{sequence} ||= $self->get_autoinc_seq($source,$col));
109 my $id = $self->_sequence_fetch( 'currval', $seq );
110 push @ids, $id;
111 }
112 return @ids;
18360aed 113}
114
115sub _dbh_get_autoinc_seq {
116 my ($self, $dbh, $source, $col) = @_;
117
032b2366 118 my $sql_maker = $self->sql_maker;
cb464582 119
e6dd7b42 120 my $source_name;
032b2366 121 if ( ref $source->name eq 'SCALAR' ) {
122 $source_name = ${$source->name};
e6dd7b42 123 }
124 else {
032b2366 125 $source_name = $source->name;
e6dd7b42 126 }
3963bf4c 127 $source_name = uc($source_name) unless $sql_maker->quote_char;
38aead8e 128
032b2366 129 # trigger_body is a LONG
130 local $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024);
131
132 # disable default bindtype
133 local $sql_maker->{bindtype} = 'normal';
134
135 # look up the correct sequence automatically
136 my ( $schema, $table ) = $source_name =~ /(\w+)\.(\w+)/;
137 my ($sql, @bind) = $sql_maker->select (
138 'ALL_TRIGGERS',
6f5f880d 139 [qw/ trigger_body table_owner trigger_name /],
032b2366 140 {
141 $schema ? (owner => $schema) : (),
142 table_name => $table || $source_name,
6f5f880d 143 triggering_event => { -like => '%INSERT%' }, # this will also catch insert_or_update
144 trigger_type => { -like => '%BEFORE%' }, # we care only about 'before' triggers
032b2366 145 status => 'ENABLED',
146 },
147 );
e6dd7b42 148
6f5f880d 149 # to find all the triggers that mention the column in question a simple
150 # regex grep since the trigger_body above is a LONG and hence not searchable
151 my @triggers = ( map
152 { my %inf; @inf{qw/body schema name/} = @$_; \%inf }
153 ( grep
154 { $_->[0] =~ /\:new\.$col/i }
155 @{ $dbh->selectall_arrayref( $sql, {}, @bind ) }
156 )
157 );
158
159 # extract all sequence names mentioned in each trigger
160 for (@triggers) {
161 $_->{sequences} = [ $_->{body} =~ / ( "? [\.\w\"\-]+ "? ) \. nextval /xig ];
162 }
163
164 my $chosen_trigger;
165
166 # if only one trigger matched things are easy
167 if (@triggers == 1) {
168
169 if ( @{$triggers[0]{sequences}} == 1 ) {
170 $chosen_trigger = $triggers[0];
171 }
172 else {
173 $self->throw_exception( sprintf (
174 "Unable to introspect trigger '%s' for column %s.%s (references multiple sequences). "
175 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
176 $triggers[0]{name},
177 $source_name,
178 $col,
179 $col,
180 ) );
181 }
182 }
183 # got more than one matching trigger - see if we can narrow it down
184 elsif (@triggers > 1) {
df6e3f5c 185
6f5f880d 186 my @candidates = grep
187 { $_->{body} =~ / into \s+ \:new\.$col /xi }
188 @triggers
189 ;
df6e3f5c 190
6f5f880d 191 if (@candidates == 1 && @{$candidates[0]{sequences}} == 1) {
192 $chosen_trigger = $candidates[0];
df6e3f5c 193 }
6f5f880d 194 else {
195 $self->throw_exception( sprintf (
196 "Unable to reliably select a BEFORE INSERT trigger for column %s.%s (possibilities: %s). "
197 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
198 $source_name,
199 $col,
200 ( join ', ', map { "'$_->{name}'" } @triggers ),
201 $col,
202 ) );
203 }
204 }
205
206 if ($chosen_trigger) {
207 my $seq_name = $chosen_trigger->{sequences}[0];
208
209 $seq_name = "$chosen_trigger->{schema}.$seq_name"
210 unless $seq_name =~ /\./;
df6e3f5c 211
212 return $seq_name;
18360aed 213 }
6f5f880d 214
215 $self->throw_exception( sprintf (
216 "No suitable BEFORE INSERT triggers found for column %s.%s. "
217 . "You need to specify the correct 'sequence' explicitly in '%s's column_info.",
218 $source_name,
219 $col,
220 $col,
221 ));
18360aed 222}
223
2e46b6eb 224sub _sequence_fetch {
225 my ( $self, $type, $seq ) = @_;
9ae966b9 226 my ($id) = $self->_get_dbh->selectrow_array("SELECT ${seq}.${type} FROM DUAL");
2e46b6eb 227 return $id;
228}
229
6dc4be0f 230sub _ping {
c2481821 231 my $self = shift;
7ba7a57d 232
6dc4be0f 233 my $dbh = $self->_dbh or return 0;
7ba7a57d 234
6dc4be0f 235 local $dbh->{RaiseError} = 1;
ecdf1ac8 236 local $dbh->{PrintError} = 0;
c2d7baef 237
52b420dd 238 return try {
ecdf1ac8 239 $dbh->do('select 1 from dual');
52b420dd 240 1;
ed7ab0f4 241 } catch {
52b420dd 242 0;
6dc4be0f 243 };
c2481821 244}
245
d789fa99 246sub _dbh_execute {
247 my $self = shift;
248 my ($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_;
249
87560ef9 250 my (@res, $tried);
dd415de8 251 my $wantarray = wantarray();
4f661051 252 my $next = $self->next::can;
87560ef9 253 do {
52b420dd 254 try {
dd415de8 255 my $exec = sub { $self->$next($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) };
256
257 if (!defined $wantarray) {
258 $exec->();
259 }
260 elsif (! $wantarray) {
261 $res[0] = $exec->();
262 }
263 else {
264 @res = $exec->();
265 }
87560ef9 266
267 $tried++;
52b420dd 268 }
269 catch {
87560ef9 270 if (! $tried and $_ =~ /ORA-01003/) {
0f0abc97 271 # ORA-01003: no statement parsed (someone changed the table somehow,
272 # invalidating your cursor.)
273 my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args);
274 delete $dbh->{CachedKids}{$sql};
d789fa99 275 }
52b420dd 276 else {
277 $self->throw_exception($_);
278 }
279 };
87560ef9 280 } while (! $tried++);
d789fa99 281
dd415de8 282 return $wantarray ? @res : $res[0];
d789fa99 283}
284
7137528d 285=head2 get_autoinc_seq
286
287Returns the sequence name for an autoincrement column
288
289=cut
290
18360aed 291sub get_autoinc_seq {
292 my ($self, $source, $col) = @_;
d4daee7b 293
373940e1 294 $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
18360aed 295}
296
8f7e044c 297=head2 datetime_parser_type
298
299This sets the proper DateTime::Format module for use with
300L<DBIx::Class::InflateColumn::DateTime>.
301
302=cut
303
304sub datetime_parser_type { return "DateTime::Format::Oracle"; }
305
9900b569 306=head2 connect_call_datetime_setup
d2a3958e 307
308Used as:
309
9900b569 310 on_connect_call => 'datetime_setup'
d2a3958e 311
8384a713 312In L<connect_info|DBIx::Class::Storage::DBI/connect_info> to set the session nls
313date, and timestamp values for use with L<DBIx::Class::InflateColumn::DateTime>
314and the necessary environment variables for L<DateTime::Format::Oracle>, which
315is used by it.
d2a3958e 316
82f6f45f 317Maximum allowable precision is used, unless the environment variables have
318already been set.
d2a3958e 319
9900b569 320These are the defaults used:
321
322 $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
323 $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF';
324 $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
325
d9e53b85 326To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
327for your timestamps, use something like this:
328
329 use Time::HiRes 'time';
330 my $ts = DateTime->from_epoch(epoch => time);
331
d2a3958e 332=cut
333
9900b569 334sub connect_call_datetime_setup {
d2a3958e 335 my $self = shift;
d2a3958e 336
337 my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
338 my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
339 'YYYY-MM-DD HH24:MI:SS.FF';
340 my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
341 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
342
7a84c41b 343 $self->_do_query(
d7a58a29 344 "alter session set nls_date_format = '$date_format'"
345 );
7a84c41b 346 $self->_do_query(
d7a58a29 347 "alter session set nls_timestamp_format = '$timestamp_format'"
348 );
7a84c41b 349 $self->_do_query(
d7a58a29 350 "alter session set nls_timestamp_tz_format='$timestamp_tz_format'"
351 );
d2a3958e 352}
353
5db2758d 354=head2 source_bind_attributes
355
356Handle LOB types in Oracle. Under a certain size (4k?), you can get away
357with the driver assuming your input is the deprecated LONG type if you
358encode it as a hex string. That ain't gonna fly at larger values, where
359you'll discover you have to do what this does.
360
361This method had to be overridden because we need to set ora_field to the
362actual column, and that isn't passed to the call (provided by Storage) to
363bind_attribute_by_data_type.
364
365According to L<DBD::Oracle>, the ora_field isn't always necessary, but
366adding it doesn't hurt, and will save your bacon if you're modifying a
367table with more than one LOB column.
368
369=cut
370
e6dd7b42 371sub source_bind_attributes
5db2758d 372{
d7a58a29 373 require DBD::Oracle;
374 my $self = shift;
375 my($source) = @_;
5db2758d 376
d7a58a29 377 my %bind_attributes;
5db2758d 378
d7a58a29 379 foreach my $column ($source->columns) {
380 my $data_type = $source->column_info($column)->{data_type} || '';
381 next unless $data_type;
5db2758d 382
d7a58a29 383 my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type);
5db2758d 384
d7a58a29 385 if ($data_type =~ /^[BC]LOB$/i) {
931e5d43 386 if ($DBD::Oracle::VERSION eq '1.23') {
387 $self->throw_exception(
388"BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
555cc3f4 389"version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
931e5d43 390 );
391 }
5db2758d 392
d7a58a29 393 $column_bind_attrs{'ora_type'} = uc($data_type) eq 'CLOB'
394 ? DBD::Oracle::ORA_CLOB()
395 : DBD::Oracle::ORA_BLOB()
396 ;
397 $column_bind_attrs{'ora_field'} = $column;
398 }
5db2758d 399
d7a58a29 400 $bind_attributes{$column} = \%column_bind_attrs;
401 }
5db2758d 402
d7a58a29 403 return \%bind_attributes;
5db2758d 404}
405
1816be4f 406sub _svp_begin {
d7a58a29 407 my ($self, $name) = @_;
408 $self->_get_dbh->do("SAVEPOINT $name");
1816be4f 409}
410
281719d2 411# Oracle automatically releases a savepoint when you start another one with the
412# same name.
413sub _svp_release { 1 }
414
415sub _svp_rollback {
d7a58a29 416 my ($self, $name) = @_;
417 $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name")
281719d2 418}
419
6c0230de 420=head2 relname_to_table_alias
421
422L<DBIx::Class> uses L<DBIx::Class::Relationship> names as table aliases in
423queries.
424
425Unfortunately, Oracle doesn't support identifiers over 30 chars in length, so
af0edca1 426the L<DBIx::Class::Relationship> name is shortened and appended with half of an
427MD5 hash.
6c0230de 428
429See L<DBIx::Class::Storage/"relname_to_table_alias">.
430
431=cut
432
433sub relname_to_table_alias {
434 my $self = shift;
435 my ($relname, $join_count) = @_;
436
437 my $alias = $self->next::method(@_);
438
63ca94e1 439 return $self->sql_maker->_shorten_identifier($alias, [$relname]);
6c0230de 440}
441
6c0bb6a7 442=head2 with_deferred_fk_checks
443
444Runs a coderef between:
445
446 alter session set constraints = deferred
447 ...
448 alter session set constraints = immediate
449
b7b18f32 450to defer foreign key checks.
451
452Constraints must be declared C<DEFERRABLE> for this to work.
6c0bb6a7 453
454=cut
455
456sub with_deferred_fk_checks {
457 my ($self, $sub) = @_;
b7b18f32 458
459 my $txn_scope_guard = $self->txn_scope_guard;
460
6c0bb6a7 461 $self->_do_query('alter session set constraints = deferred');
54161a15 462
b7b18f32 463 my $sg = Scope::Guard->new(sub {
464 $self->_do_query('alter session set constraints = immediate');
465 });
281719d2 466
6298a324 467 return
468 preserve_context { $sub->() } after => sub { $txn_scope_guard->commit };
281719d2 469}
470
c0024355 471=head1 ATTRIBUTES
472
473Following additional attributes can be used in resultsets.
474
6b2fbbf0 475=head2 connect_by or connect_by_nocycle
c0024355 476
477=over 4
478
479=item Value: \%connect_by
480
481=back
482
483A hashref of conditions used to specify the relationship between parent rows
484and child rows of the hierarchy.
485
6b2fbbf0 486
c0024355 487 connect_by => { parentid => 'prior personid' }
488
489 # adds a connect by statement to the query:
490 # SELECT
491 # me.persionid me.firstname, me.lastname, me.parentid
492 # FROM
493 # person me
494 # CONNECT BY
495 # parentid = prior persionid
6b2fbbf0 496
c0024355 497
6b2fbbf0 498 connect_by_nocycle => { parentid => 'prior personid' }
2ba03b16 499
6b2fbbf0 500 # adds a connect by statement to the query:
501 # SELECT
502 # me.persionid me.firstname, me.lastname, me.parentid
503 # FROM
504 # person me
505 # CONNECT BY NOCYCLE
506 # parentid = prior persionid
2ba03b16 507
508
c0024355 509=head2 start_with
510
511=over 4
512
513=item Value: \%condition
514
515=back
516
517A hashref of conditions which specify the root row(s) of the hierarchy.
518
519It uses the same syntax as L<DBIx::Class::ResultSet/search>
520
521 start_with => { firstname => 'Foo', lastname => 'Bar' }
522
523 # SELECT
524 # me.persionid me.firstname, me.lastname, me.parentid
525 # FROM
526 # person me
527 # START WITH
528 # firstname = 'foo' and lastname = 'bar'
529 # CONNECT BY
530 # parentid = prior persionid
531
532=head2 order_siblings_by
533
534=over 4
535
536=item Value: ($order_siblings_by | \@order_siblings_by)
537
538=back
539
540Which column(s) to order the siblings by.
541
542It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
543
544 'order_siblings_by' => 'firstname ASC'
545
546 # SELECT
547 # me.persionid me.firstname, me.lastname, me.parentid
548 # FROM
549 # person me
550 # CONNECT BY
551 # parentid = prior persionid
552 # ORDER SIBLINGS BY
553 # firstname ASC
554
7a84c41b 555=head1 AUTHOR
18360aed 556
7a84c41b 557See L<DBIx::Class/CONTRIBUTORS>.
18360aed 558
559=head1 LICENSE
560
561You may distribute this code under the same terms as Perl itself.
562
563=cut
7137528d 564
5651;