File Coverage

blib/lib/DBIx/Class/SQLMaker/Oracle.pm
Criterion Covered Total %
statement 13 13 100.0
branch 1 2 50.0
condition n/a
subroutine 5 5 100.0
pod n/a
total 19 20 95.0


line stmt bran cond sub pod time code
1             package # Hide from PAUSE
2             DBIx::Class::SQLMaker::Oracle;
3              
4 2     2   13 use warnings;
  2         4  
  2         51  
5 2     2   8 use strict;
  2         3  
  2         35  
6              
7 2     2   8 use base qw( DBIx::Class::SQLMaker );
  2         4  
  2         177  
8              
9             BEGIN {
10 2     2   12 use DBIx::Class::Optional::Dependencies;
  2         3  
  2         103  
11 2 50   2   13 die('The following extra modules are required for Oracle-based Storages ' . DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener') . "\n" )
12             unless DBIx::Class::Optional::Dependencies->req_ok_for ('id_shortener');
13             }
14              
15             sub new {
16             my $self = shift;
17             my %opts = (ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
18             push @{$opts{special_ops}}, {
19             regex => qr/^prior$/i,
20             handler => '_where_field_PRIOR',
21             };
22              
23             $self->next::method(\%opts);
24             }
25              
26             sub _assemble_binds {
27             my $self = shift;
28             return map { @{ (delete $self->{"${_}_bind"}) || [] } } (qw/pre_select select from where oracle_connect_by group having order limit/);
29             }
30              
31              
32             sub _parse_rs_attrs {
33             my $self = shift;
34             my ($rs_attrs) = @_;
35              
36             my ($cb_sql, @cb_bind) = $self->_connect_by($rs_attrs);
37             push @{$self->{oracle_connect_by_bind}}, @cb_bind;
38              
39             my $sql = $self->next::method(@_);
40              
41             return "$cb_sql $sql";
42             }
43              
44             sub _connect_by {
45             my ($self, $attrs) = @_;
46              
47             my $sql = '';
48             my @bind;
49              
50             if ( ref($attrs) eq 'HASH' ) {
51             if ( $attrs->{'start_with'} ) {
52             my ($ws, @wb) = $self->_recurse_where( $attrs->{'start_with'} );
53             $sql .= $self->_sqlcase(' start with ') . $ws;
54             push @bind, @wb;
55             }
56             if ( my $connect_by = $attrs->{'connect_by'} || $attrs->{'connect_by_nocycle'} ) {
57             my ($connect_by_sql, @connect_by_sql_bind) = $self->_recurse_where( $connect_by );
58             $sql .= sprintf(" %s %s",
59             ( $attrs->{'connect_by_nocycle'} ) ? $self->_sqlcase('connect by nocycle')
60             : $self->_sqlcase('connect by'),
61             $connect_by_sql,
62             );
63             push @bind, @connect_by_sql_bind;
64             }
65             if ( $attrs->{'order_siblings_by'} ) {
66             $sql .= $self->_order_siblings_by( $attrs->{'order_siblings_by'} );
67             }
68             }
69              
70             return wantarray ? ($sql, @bind) : $sql;
71             }
72              
73             sub _order_siblings_by {
74             my ( $self, $arg ) = @_;
75              
76             my ( @sql, @bind );
77             for my $c ( $self->_order_by_chunks($arg) ) {
78             if (ref $c) {
79             push @sql, shift @$c;
80             push @bind, @$c;
81             }
82             else {
83             push @sql, $c;
84             }
85             }
86              
87             my $sql =
88             @sql
89             ? sprintf( '%s %s', $self->_sqlcase(' order siblings by'), join( ', ', @sql ) )
90             : '';
91              
92             return wantarray ? ( $sql, @bind ) : $sql;
93             }
94              
95             # we need to add a '=' only when PRIOR is used against a column directly
96             # i.e. when it is invoked by a special_op callback
97             sub _where_field_PRIOR {
98             my ($self, $lhs, $op, $rhs) = @_;
99             my ($sql, @bind) = $self->_recurse_where ($rhs);
100              
101             $sql = sprintf ('%s = %s %s ',
102             $self->_convert($self->_quote($lhs)),
103             $self->_sqlcase ($op),
104             $sql
105             );
106              
107             return ($sql, @bind);
108             }
109              
110             # use this codepath to hook all identifiers and mangle them if necessary
111             # this is invoked regardless of quoting being on or off
112             sub _quote {
113             my ($self, $label) = @_;
114              
115             return '' unless defined $label;
116             return ${$label} if ref($label) eq 'SCALAR';
117              
118             $label =~ s/ ( [^\.]{31,} ) /$self->_shorten_identifier($1)/gxe;
119              
120             $self->next::method($label);
121             }
122              
123             # this takes an identifier and shortens it if necessary
124             # optionally keywords can be passed as an arrayref to generate useful
125             # identifiers
126             sub _shorten_identifier {
127             my ($self, $to_shorten, $keywords) = @_;
128              
129             # 30 characters is the identifier limit for Oracle
130             my $max_len = 30;
131             # we want at least 10 characters of the base36 md5
132             my $min_entropy = 10;
133              
134             my $max_trunc = $max_len - $min_entropy - 1;
135              
136             return $to_shorten
137             if length($to_shorten) <= $max_len;
138              
139             $self->throw_exception("'keywords' needs to be an arrayref")
140             if defined $keywords && ref $keywords ne 'ARRAY';
141              
142             # if no keywords are passed use the identifier as one
143             my @keywords = @{$keywords || []};
144             @keywords = $to_shorten unless @keywords;
145              
146             # get a base36 md5 of the identifier
147             require Digest::MD5;
148             require Math::BigInt;
149             require Math::Base36;
150             my $b36sum = Math::Base36::encode_base36(
151             Math::BigInt->from_hex (
152             '0x' . Digest::MD5::md5_hex ($to_shorten)
153             )
154             );
155              
156             # switch from perl to java
157             # get run-length
158             my ($concat_len, @lengths);
159             for (@keywords) {
160             $_ = ucfirst (lc ($_));
161             $_ =~ s/\_+(\w)/uc ($1)/eg;
162              
163             push @lengths, length ($_);
164             $concat_len += $lengths[-1];
165             }
166              
167             # if we are still too long - try to disemvowel non-capitals (not keyword starts)
168             if ($concat_len > $max_trunc) {
169             $concat_len = 0;
170             @lengths = ();
171              
172             for (@keywords) {
173             $_ =~ s/[aeiou]//g;
174              
175             push @lengths, length ($_);
176             $concat_len += $lengths[-1];
177             }
178             }
179              
180             # still too long - just start cutting proportionally
181             if ($concat_len > $max_trunc) {
182             my $trim_ratio = $max_trunc / $concat_len;
183              
184             for my $i (0 .. $#keywords) {
185             $keywords[$i] = substr ($keywords[$i], 0, int ($trim_ratio * $lengths[$i] ) );
186             }
187             }
188              
189             my $fin = join ('', @keywords);
190             my $fin_len = length $fin;
191              
192             return sprintf ('%s_%s',
193             $fin,
194             substr ($b36sum, 0, $max_len - $fin_len - 1),
195             );
196             }
197              
198             sub _unqualify_colname {
199             my ($self, $fqcn) = @_;
200              
201             return $self->_shorten_identifier($self->next::method($fqcn));
202             }
203              
204             #
205             # Oracle has a different INSERT...RETURNING syntax
206             #
207              
208             sub _insert_returning {
209             my ($self, $options) = @_;
210              
211             my $f = $options->{returning};
212              
213             my ($f_list, @f_names) = do {
214             if (! ref $f) {
215             (
216             $self->_quote($f),
217             $f,
218             )
219             }
220             elsif (ref $f eq 'ARRAY') {
221             (
222             (join ', ', map { $self->_quote($_) } @$f),
223             @$f,
224             )
225             }
226             elsif (ref $f eq 'SCALAR') {
227             (
228             $$f,
229             $$f,
230             )
231             }
232             else {
233             $self->throw_exception("Unsupported INSERT RETURNING option $f");
234             }
235             };
236              
237             my $rc_ref = $options->{returning_container}
238             or $self->throw_exception('No returning container supplied for IR values');
239              
240             @$rc_ref = (undef) x @f_names;
241              
242             return (
243             ( join (' ',
244             $self->_sqlcase(' returning'),
245             $f_list,
246             $self->_sqlcase('into'),
247             join (', ', ('?') x @f_names ),
248             )),
249             map {
250             $self->{bindtype} eq 'columns'
251             ? [ $f_names[$_] => \$rc_ref->[$_] ]
252             : \$rc_ref->[$_]
253             } (0 .. $#f_names),
254             );
255             }
256              
257             1;