Add PRIOR as special and unary op to SQLAHacks::Oracle and use _recurse_where to...
[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;
5
7137528d 6=head1 NAME
7
7a84c41b 8DBIx::Class::Storage::DBI::Oracle::Generic - Oracle Support for DBIx::Class
7137528d 9
10=head1 SYNOPSIS
11
12 # In your table classes
13 __PACKAGE__->load_components(qw/PK::Auto Core/);
2e46b6eb 14 __PACKAGE__->add_columns({ id => { sequence => 'mysequence', auto_nextval => 1 } });
7137528d 15 __PACKAGE__->set_primary_key('id');
16 __PACKAGE__->sequence('mysequence');
17
c0024355 18 # Somewhere in your Code
19 # add some data to a table with a hierarchical relationship
20 $schema->resultset('Person')->create ({
21 firstname => 'foo',
22 lastname => 'bar',
23 children => [
24 {
25 firstname => 'child1',
26 lastname => 'bar',
27 children => [
28 {
29 firstname => 'grandchild',
30 lastname => 'bar',
31 }
32 ],
33 },
34 {
35 firstname => 'child2',
36 lastname => 'bar',
37 },
38 ],
39 });
40
41 # select from the hierarchical relationship
42 my $rs = $schema->resultset('Person')->search({},
43 {
44 'start_with' => { 'firstname' => 'foo', 'lastname' => 'bar' },
45 'connect_by' => { 'parentid' => 'prior persionid'},
46 'order_siblings_by' => 'firstname ASC',
47 };
48 );
49
50 # this will select the whole tree starting from person "foo bar", creating
51 # following query:
52 # SELECT
53 # me.persionid me.firstname, me.lastname, me.parentid
54 # FROM
55 # person me
56 # START WITH
57 # firstname = 'foo' and lastname = 'bar'
58 # CONNECT BY
59 # parentid = prior persionid
60 # ORDER SIBLINGS BY
61 # firstname ASC
62
7137528d 63=head1 DESCRIPTION
64
c0024355 65This class implements autoincrements for Oracle and adds support for Oracle
66specific hierarchical queries.
7137528d 67
68=head1 METHODS
69
70=cut
71
db56cf3d 72use base qw/DBIx::Class::Storage::DBI/;
2ad62d97 73use mro 'c3';
18360aed 74
c0024355 75__PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::Oracle');
76
18360aed 77sub _dbh_last_insert_id {
2e46b6eb 78 my ($self, $dbh, $source, @columns) = @_;
79 my @ids = ();
80 foreach my $col (@columns) {
81 my $seq = ($source->column_info($col)->{sequence} ||= $self->get_autoinc_seq($source,$col));
82 my $id = $self->_sequence_fetch( 'currval', $seq );
83 push @ids, $id;
84 }
85 return @ids;
18360aed 86}
87
88sub _dbh_get_autoinc_seq {
89 my ($self, $dbh, $source, $col) = @_;
90
91 # look up the correct sequence automatically
92 my $sql = q{
93 SELECT trigger_body FROM ALL_TRIGGERS t
94 WHERE t.table_name = ?
95 AND t.triggering_event = 'INSERT'
96 AND t.status = 'ENABLED'
97 };
98
99 # trigger_body is a LONG
7a84c41b 100 local $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024);
18360aed 101
cb464582 102 my $sth;
103
e6dd7b42 104 my $source_name;
105 if ( ref $source->name ne 'SCALAR' ) {
106 $source_name = $source->name;
107 }
108 else {
109 $source_name = ${$source->name};
110 }
111
cb464582 112 # check for fully-qualified name (eg. SCHEMA.TABLENAME)
e6dd7b42 113 if ( my ( $schema, $table ) = $source_name =~ /(\w+)\.(\w+)/ ) {
cb464582 114 $sql = q{
115 SELECT trigger_body FROM ALL_TRIGGERS t
116 WHERE t.owner = ? AND t.table_name = ?
117 AND t.triggering_event = 'INSERT'
118 AND t.status = 'ENABLED'
119 };
120 $sth = $dbh->prepare($sql);
121 $sth->execute( uc($schema), uc($table) );
122 }
123 else {
124 $sth = $dbh->prepare($sql);
e6dd7b42 125 $sth->execute( uc( $source_name ) );
cb464582 126 }
18360aed 127 while (my ($insert_trigger) = $sth->fetchrow_array) {
128 return uc($1) if $insert_trigger =~ m!(\w+)\.nextval!i; # col name goes here???
129 }
66cab05c 130 $self->throw_exception("Unable to find a sequence INSERT trigger on table '" . $source->name . "'.");
18360aed 131}
132
2e46b6eb 133sub _sequence_fetch {
134 my ( $self, $type, $seq ) = @_;
9ae966b9 135 my ($id) = $self->_get_dbh->selectrow_array("SELECT ${seq}.${type} FROM DUAL");
2e46b6eb 136 return $id;
137}
138
6dc4be0f 139sub _ping {
c2481821 140 my $self = shift;
7ba7a57d 141
6dc4be0f 142 my $dbh = $self->_dbh or return 0;
7ba7a57d 143
6dc4be0f 144 local $dbh->{RaiseError} = 1;
c2d7baef 145
6dc4be0f 146 eval {
147 $dbh->do("select 1 from dual");
148 };
7ba7a57d 149
6dc4be0f 150 return $@ ? 0 : 1;
c2481821 151}
152
d789fa99 153sub _dbh_execute {
154 my $self = shift;
155 my ($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_;
156
157 my $wantarray = wantarray;
d789fa99 158
c2d7baef 159 my (@res, $exception, $retried);
160
0f0abc97 161 RETRY: {
162 do {
163 eval {
164 if ($wantarray) {
c3515436 165 @res = $self->next::method(@_);
0f0abc97 166 } else {
c3515436 167 $res[0] = $self->next::method(@_);
0f0abc97 168 }
169 };
170 $exception = $@;
171 if ($exception =~ /ORA-01003/) {
172 # ORA-01003: no statement parsed (someone changed the table somehow,
173 # invalidating your cursor.)
174 my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args);
175 delete $dbh->{CachedKids}{$sql};
d789fa99 176 } else {
0f0abc97 177 last RETRY;
d789fa99 178 }
0f0abc97 179 } while (not $retried++);
180 }
d789fa99 181
182 $self->throw_exception($exception) if $exception;
183
184 wantarray ? @res : $res[0]
185}
186
7137528d 187=head2 get_autoinc_seq
188
189Returns the sequence name for an autoincrement column
190
191=cut
192
18360aed 193sub get_autoinc_seq {
194 my ($self, $source, $col) = @_;
d4daee7b 195
373940e1 196 $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
18360aed 197}
198
7137528d 199=head2 columns_info_for
200
201This wraps the superclass version of this method to force table
202names to uppercase
203
204=cut
205
18360aed 206sub columns_info_for {
207 my ($self, $table) = @_;
208
209 $self->next::method(uc($table));
210}
211
8f7e044c 212=head2 datetime_parser_type
213
214This sets the proper DateTime::Format module for use with
215L<DBIx::Class::InflateColumn::DateTime>.
216
217=cut
218
219sub datetime_parser_type { return "DateTime::Format::Oracle"; }
220
9900b569 221=head2 connect_call_datetime_setup
d2a3958e 222
223Used as:
224
9900b569 225 on_connect_call => 'datetime_setup'
d2a3958e 226
82f6f45f 227In L<DBIx::Class::Storage::DBI/connect_info> to set the session nls date, and
228timestamp values for use with L<DBIx::Class::InflateColumn::DateTime> and the
229necessary environment variables for L<DateTime::Format::Oracle>, which is used
230by it.
d2a3958e 231
82f6f45f 232Maximum allowable precision is used, unless the environment variables have
233already been set.
d2a3958e 234
9900b569 235These are the defaults used:
236
237 $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
238 $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF';
239 $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
240
d9e53b85 241To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
242for your timestamps, use something like this:
243
244 use Time::HiRes 'time';
245 my $ts = DateTime->from_epoch(epoch => time);
246
d2a3958e 247=cut
248
9900b569 249sub connect_call_datetime_setup {
d2a3958e 250 my $self = shift;
d2a3958e 251
252 my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
253 my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
254 'YYYY-MM-DD HH24:MI:SS.FF';
255 my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
256 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
257
7a84c41b 258 $self->_do_query("alter session set nls_date_format = '$date_format'");
259 $self->_do_query(
260"alter session set nls_timestamp_format = '$timestamp_format'");
261 $self->_do_query(
262"alter session set nls_timestamp_tz_format='$timestamp_tz_format'");
d2a3958e 263}
264
5db2758d 265=head2 source_bind_attributes
266
267Handle LOB types in Oracle. Under a certain size (4k?), you can get away
268with the driver assuming your input is the deprecated LONG type if you
269encode it as a hex string. That ain't gonna fly at larger values, where
270you'll discover you have to do what this does.
271
272This method had to be overridden because we need to set ora_field to the
273actual column, and that isn't passed to the call (provided by Storage) to
274bind_attribute_by_data_type.
275
276According to L<DBD::Oracle>, the ora_field isn't always necessary, but
277adding it doesn't hurt, and will save your bacon if you're modifying a
278table with more than one LOB column.
279
280=cut
281
e6dd7b42 282sub source_bind_attributes
5db2758d 283{
efc5bf40 284 require DBD::Oracle;
5db2758d 285 my $self = shift;
286 my($source) = @_;
287
288 my %bind_attributes;
289
290 foreach my $column ($source->columns) {
291 my $data_type = $source->column_info($column)->{data_type} || '';
292 next unless $data_type;
293
294 my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type);
295
296 if ($data_type =~ /^[BC]LOB$/i) {
0d1207e8 297 $column_bind_attrs{'ora_type'} = uc($data_type) eq 'CLOB' ?
298 DBD::Oracle::ORA_CLOB() :
299 DBD::Oracle::ORA_BLOB();
5db2758d 300 $column_bind_attrs{'ora_field'} = $column;
301 }
302
303 $bind_attributes{$column} = \%column_bind_attrs;
304 }
305
306 return \%bind_attributes;
307}
308
1816be4f 309sub _svp_begin {
310 my ($self, $name) = @_;
311
e33b954c 312 $self->_get_dbh->do("SAVEPOINT $name");
1816be4f 313}
314
281719d2 315# Oracle automatically releases a savepoint when you start another one with the
316# same name.
317sub _svp_release { 1 }
318
319sub _svp_rollback {
320 my ($self, $name) = @_;
321
9ae966b9 322 $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name")
281719d2 323}
324
c0024355 325sub _select_args {
326 my ($self, $ident, $select, $where, $attrs) = @_;
327
328 my $connect_by_args = {};
329 if ( $attrs->{connect_by} || $attrs->{start_with} || $attrs->{order_siblings_by} ) {
330 $connect_by_args = {
331 connect_by => $attrs->{connect_by},
332 start_with => $attrs->{start_with},
333 order_siblings_by => $attrs->{order_siblings_by},
334 }
335 }
336
337 my @rv = $self->next::method($ident, $select, $where, $attrs);
338
339 return (@rv, $connect_by_args);
340}
341
342=head1 ATTRIBUTES
343
344Following additional attributes can be used in resultsets.
345
346=head2 connect_by
347
348=over 4
349
350=item Value: \%connect_by
351
352=back
353
354A hashref of conditions used to specify the relationship between parent rows
355and child rows of the hierarchy.
356
357 connect_by => { parentid => 'prior personid' }
358
359 # adds a connect by statement to the query:
360 # SELECT
361 # me.persionid me.firstname, me.lastname, me.parentid
362 # FROM
363 # person me
364 # CONNECT BY
365 # parentid = prior persionid
366
367=head2 start_with
368
369=over 4
370
371=item Value: \%condition
372
373=back
374
375A hashref of conditions which specify the root row(s) of the hierarchy.
376
377It uses the same syntax as L<DBIx::Class::ResultSet/search>
378
379 start_with => { firstname => 'Foo', lastname => 'Bar' }
380
381 # SELECT
382 # me.persionid me.firstname, me.lastname, me.parentid
383 # FROM
384 # person me
385 # START WITH
386 # firstname = 'foo' and lastname = 'bar'
387 # CONNECT BY
388 # parentid = prior persionid
389
390=head2 order_siblings_by
391
392=over 4
393
394=item Value: ($order_siblings_by | \@order_siblings_by)
395
396=back
397
398Which column(s) to order the siblings by.
399
400It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
401
402 'order_siblings_by' => 'firstname ASC'
403
404 # SELECT
405 # me.persionid me.firstname, me.lastname, me.parentid
406 # FROM
407 # person me
408 # CONNECT BY
409 # parentid = prior persionid
410 # ORDER SIBLINGS BY
411 # firstname ASC
412
7a84c41b 413=head1 AUTHOR
18360aed 414
7a84c41b 415See L<DBIx::Class/CONTRIBUTORS>.
18360aed 416
417=head1 LICENSE
418
419You may distribute this code under the same terms as Perl itself.
420
421=cut
7137528d 422
4231;