Switch sql_maker_class and datetime_parser_type to component_class accessors
[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 {
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
63ca94e1 471 return $self->sql_maker->_shorten_identifier($alias, [$relname]);
6c0230de 472}
473
6c0bb6a7 474=head2 with_deferred_fk_checks
475
476Runs a coderef between:
477
478 alter session set constraints = deferred
479 ...
480 alter session set constraints = immediate
481
b7b18f32 482to defer foreign key checks.
483
484Constraints must be declared C<DEFERRABLE> for this to work.
6c0bb6a7 485
486=cut
487
488sub with_deferred_fk_checks {
489 my ($self, $sub) = @_;
b7b18f32 490
491 my $txn_scope_guard = $self->txn_scope_guard;
492
6c0bb6a7 493 $self->_do_query('alter session set constraints = deferred');
54161a15 494
b7b18f32 495 my $sg = Scope::Guard->new(sub {
496 $self->_do_query('alter session set constraints = immediate');
497 });
281719d2 498
6298a324 499 return
500 preserve_context { $sub->() } after => sub { $txn_scope_guard->commit };
281719d2 501}
502
c0024355 503=head1 ATTRIBUTES
504
505Following additional attributes can be used in resultsets.
506
6b2fbbf0 507=head2 connect_by or connect_by_nocycle
c0024355 508
509=over 4
510
511=item Value: \%connect_by
512
513=back
514
515A hashref of conditions used to specify the relationship between parent rows
516and child rows of the hierarchy.
517
6b2fbbf0 518
c0024355 519 connect_by => { parentid => 'prior personid' }
520
521 # adds a connect by statement to the query:
522 # SELECT
523 # me.persionid me.firstname, me.lastname, me.parentid
524 # FROM
525 # person me
526 # CONNECT BY
527 # parentid = prior persionid
6b2fbbf0 528
c0024355 529
6b2fbbf0 530 connect_by_nocycle => { parentid => 'prior personid' }
2ba03b16 531
6b2fbbf0 532 # adds a connect by statement to the query:
533 # SELECT
534 # me.persionid me.firstname, me.lastname, me.parentid
535 # FROM
536 # person me
537 # CONNECT BY NOCYCLE
538 # parentid = prior persionid
2ba03b16 539
540
c0024355 541=head2 start_with
542
543=over 4
544
545=item Value: \%condition
546
547=back
548
549A hashref of conditions which specify the root row(s) of the hierarchy.
550
551It uses the same syntax as L<DBIx::Class::ResultSet/search>
552
553 start_with => { firstname => 'Foo', lastname => 'Bar' }
554
555 # SELECT
556 # me.persionid me.firstname, me.lastname, me.parentid
557 # FROM
558 # person me
559 # START WITH
560 # firstname = 'foo' and lastname = 'bar'
561 # CONNECT BY
562 # parentid = prior persionid
563
564=head2 order_siblings_by
565
566=over 4
567
568=item Value: ($order_siblings_by | \@order_siblings_by)
569
570=back
571
572Which column(s) to order the siblings by.
573
574It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
575
576 'order_siblings_by' => 'firstname ASC'
577
578 # SELECT
579 # me.persionid me.firstname, me.lastname, me.parentid
580 # FROM
581 # person me
582 # CONNECT BY
583 # parentid = prior persionid
584 # ORDER SIBLINGS BY
585 # firstname ASC
586
7a84c41b 587=head1 AUTHOR
18360aed 588
7a84c41b 589See L<DBIx::Class/CONTRIBUTORS>.
18360aed 590
591=head1 LICENSE
592
593You may distribute this code under the same terms as Perl itself.
594
595=cut
7137528d 596
5971;