File Coverage

blib/lib/DBIx/Pg/CallFunction.pm
Criterion Covered Total %
statement 15 117 12.8
branch 0 68 0.0
condition 0 12 0.0
subroutine 5 12 41.6
pod 1 2 50.0
total 21 211 9.9


line stmt bran cond sub pod time code
1             package DBIx::Pg::CallFunction;
2             our $VERSION = '0.019';
3 2     2   41906 use 5.008;
  2         8  
  2         148  
4              
5             =head1 NAME
6              
7             DBIx::Pg::CallFunction - Simple interface for calling PostgreSQL functions from Perl
8              
9             =head1 VERSION
10              
11             version 0.019
12              
13             =head1 SYNOPSIS
14              
15             use DBI;
16             use DBIx::Pg::CallFunction;
17              
18             my $dbh = DBI->connect("dbi:Pg:dbname=joel", 'joel', '');
19             my $pg = DBIx::Pg::CallFunction->new($dbh);
20              
21             Returning single-row single-column values:
22              
23             my $userid = $pg->get_userid_by_username({'username' => 'joel'});
24             # returns scalar 123
25              
26             Returning multi-row single-column values:
27              
28             my $hosts = $pg->get_user_hosts({userid => 123});
29             # returns array ref ['127.0.0.1', '192.168.0.1', ...]
30              
31             Returning single-row multi-column values:
32              
33             my $user_details = $pg->get_user_details({userid => 123});
34             # returns hash ref { firstname=>..., lastname=>... }
35              
36             Returning multi-row multi-column values:
37              
38             my $user_friends = $pg->get_user_friends({userid => 123});
39             # returns array ref of hash refs [{ userid=>..., firstname=>..., lastname=>...}, ...]
40              
41             =head1 DESCRIPTION
42              
43             This module provides a simple efficient way to call PostgreSQL functions
44             with from Perl code. It only support functions with named arguments, or
45             functions with no arguments at all. This limitation reduces the mapping
46             complexity, as multiple functions in PostgreSQL can share the same name,
47             but with different input argument types.
48              
49             Please see L for an example on how to use this module.
50              
51             =head1 CONSTRUCTOR METHODS
52              
53             The following constructor methods are available:
54              
55             =over 4
56              
57             =item my $pg = DBIx::Pg::CallFunction->new($dbh, [$hashref])
58              
59             This method constructs a new C object and returns it.
60              
61             $dbh is a handle to your database connection.
62              
63             $hashref is an optional reference to a hash containing configuration parameters.
64             If it not present, the default values will be used.
65              
66             =back
67              
68             =head2 CONFIGURATION PARAMETERS
69              
70             The following configuration parameters are available:
71              
72             =over 4
73              
74             =item EnableFunctionLookupCache
75              
76             When enabled, the procedure returns set for each function will be cached.
77             This is disabled by default.
78              
79             =item RaiseError
80              
81             By default, this is enabled. It is used like L.
82              
83             =back
84              
85             =head1 REQUEST METHODS
86              
87             =over 4
88              
89             =item my $output = $pg->$name_of_stored_procedure($hashref_of_input_arguments)
90              
91             =item my $output = $pg->$name_of_stored_procedure($hashref_of_input_arguments, $namespace)
92              
93             =back
94              
95             =head1 SEE ALSO
96              
97             This module is built on top of L, and
98             you need to use that module (and the appropriate DBD::Pg driver)
99             to establish a database connection.
100              
101             There is another module providing about the same functionality,
102             but without support for named arguments for PostgreSQL.
103             Have a look at this one if you need to access functions
104             without named arguments, or if you are using Oracle:
105              
106             L
107              
108             =head1 LIMITATIONS
109              
110             Requires PostgreSQL 9.0 or later.
111             Only supports stored procedures / functions with
112             named input arguments.
113              
114             =head1 AUTHOR
115              
116             Joel Jacobson L
117              
118             =head1 COPYRIGHT
119              
120             Copyright (c) Joel Jacobson, Sweden, 2012. All rights reserved.
121              
122             This software is released under the MIT license cited below.
123              
124             =head2 The "MIT" License
125              
126             Permission is hereby granted, free of charge, to any person obtaining a copy
127             of this software and associated documentation files (the "Software"), to deal
128             in the Software without restriction, including without limitation the rights
129             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
130             copies of the Software, and to permit persons to whom the Software is
131             furnished to do so, subject to the following conditions:
132              
133             The above copyright notice and this permission notice shall be included in
134             all copies or substantial portions of the Software.
135              
136             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
137             OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
138             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
139             THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
140             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
141             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
142             DEALINGS IN THE SOFTWARE.
143              
144             =cut
145              
146 2     2   12 use strict;
  2         4  
  2         66  
147 2     2   28 use warnings;
  2         3  
  2         56  
148              
149 2     2   11 use Carp;
  2         2  
  2         184  
150 2     2   18164 use DBI;
  2         59659  
  2         3029  
151              
152             our $AUTOLOAD;
153              
154             sub new
155             {
156 0     0 1   my $class = shift;
157 0           my $self =
158             {
159             dbh => shift,
160             RaiseError => 1,
161             EnableFunctionLookupCache => 0,
162              
163             prosetret_cache => {}
164             };
165              
166 0           my $params = shift;
167 0 0         if (defined $params)
168             {
169 0 0         $self->{RaiseError} = delete $params->{RaiseError} if exists $params->{RaiseError};
170 0 0         $self->{EnableFunctionLookupCache} = delete $params->{EnableFunctionLookupCache} if exists $params->{EnableFunctionLookupCache};
171              
172             # If there were any unrecognized parameters left, report one of them
173 0 0         if (scalar keys %{$params} > 0)
  0            
174             {
175 0           my $param = shift @{keys %{$params}};
  0            
  0            
176 0           croak "unrecognized parameter $param";
177             }
178             }
179              
180 0           bless $self, $class;
181 0           return $self;
182             }
183              
184             sub set_dbh
185             {
186 0     0 0   my ($self, $dbh) = @_;
187 0           $self->{dbh} = $dbh;
188             }
189              
190             sub AUTOLOAD
191             {
192 0     0     my $self = shift;
193 0           my $args = shift;
194 0           my $namespace = shift;
195 0           my $name = $AUTOLOAD;
196 0 0         return if ($name =~ /DESTROY$/);
197 0           $name =~ s!^.*::([^:]+)$!$1!;
198 0           return $self->_call($name, $args, $namespace);
199             }
200              
201             # Calculates a cache key for a function, given its signature.
202             #
203             # The caller should sort $argnames before passing them to us.
204             sub _calculate_proretset_cache_key
205             {
206 0     0     my ($self, $name, $argnames, $namespace) = @_;
207              
208 0           return (defined $namespace ? $namespace : "").".".
209 0 0         $name."(".join(",", @{$argnames}).")";
210             }
211              
212              
213             # Because there is no way for us to do "proper" cache invalidation, we have to
214             # rely on detecting the SQLSTATEs of the cases where the cache entry might be
215             # stale. Currently, these cases are:
216             #
217             # 1) A cached function gets dropped. (SQLSTATE undefined_function)
218             # 2) A new function with the same signature is introduced (SQLSTATE
219             # ambiguous_function)
220             sub _invalidate_proretset_cache_entry
221             {
222 0     0     my ($self, $name, $argnames, $namespace) = @_;
223              
224 0           my $cachekey = $self->_calculate_proretset_cache_key($name, $argnames, $namespace);
225 0           delete $self->{proretset_cache}->{$cachekey};
226             }
227              
228             sub _proretset
229             {
230             # Returns the value of pg_catalog.pg_proc.proretset for the function.
231             # "proretset" is short for procedure returns set.
232             # If 1, the function returns multiple rows, or zero rows.
233             # If 0, the function always returns exactly one row.
234 0     0     my ($self, $name, $argnames, $namespace) = @_;
235              
236 0           my $cachekey = undef;
237              
238             # do a cache lookup if the caller asked for that
239 0 0         if ($self->{EnableFunctionLookupCache})
240             {
241 0           $cachekey = $self->_calculate_proretset_cache_key($name, $argnames, $namespace);
242 0 0         if (exists ($self->{proretset_cache}->{$cachekey}))
243             {
244 0           my $cached = $self->{proretset_cache}->{$cachekey};
245 0           return $cached;
246             }
247             }
248              
249 0           my $get_proretset;
250 0 0         if (@$argnames == 0)
251             {
252             # no arguments
253 0           $get_proretset = $self->{dbh}->prepare_cached("
254             SELECT pg_catalog.pg_proc.proretset
255             FROM pg_catalog.pg_proc
256             INNER JOIN pg_catalog.pg_namespace ON (pg_catalog.pg_namespace.oid = pg_catalog.pg_proc.pronamespace)
257             WHERE (?::text IS NULL OR pg_catalog.pg_namespace.nspname = ?::text)
258             AND pg_catalog.pg_proc.proname = ?::text
259             AND pg_catalog.pg_proc.pronargs = 0
260             ");
261 0           $get_proretset->execute($namespace,$namespace,$name);
262             }
263             else
264             {
265 0 0         $get_proretset = $self->{dbh}->prepare_cached("
266             WITH
267             -- Unnest the proargname and proargmode
268             -- arrays, so we get one argument per row,
269             -- allowing us to select only the IN
270             -- arguments and build new arrays.
271             NamedInputArgumentFunctions AS (
272             -- For functions with INOUT/OUT arguments,
273             -- proargmodes is an array where each
274             -- position matches proargname and
275             -- indicates if its an IN, OUT or INOUT
276             -- argument.
277             SELECT
278             pg_catalog.pg_proc.oid,
279             pg_catalog.pg_proc.proname,
280             pg_catalog.pg_proc.proretset,
281             pg_catalog.pg_proc.pronargdefaults,
282             unnest(pg_catalog.pg_proc.proargnames) AS proargname,
283             unnest(pg_catalog.pg_proc.proargmodes) AS proargmode
284             FROM pg_catalog.pg_proc
285             INNER JOIN pg_catalog.pg_namespace ON (pg_catalog.pg_namespace.oid = pg_catalog.pg_proc.pronamespace)
286             WHERE (?::name IS NULL OR pg_catalog.pg_namespace.nspname = ?::name)
287             AND pg_catalog.pg_proc.proname = ?::name
288             AND pg_catalog.pg_proc.proargnames IS NOT NULL
289             AND pg_catalog.pg_proc.proargmodes IS NOT NULL
290             ),
291             OnlyINandINOUTArguments AS (
292             -- Select only the IN and INOUT
293             -- arguments and build new arrays
294             SELECT
295             oid,
296             proname,
297             proretset,
298             pronargdefaults,
299             array_agg(proargname) AS proargnames
300             FROM NamedInputArgumentFunctions
301             WHERE proargmode IN ('i','b')
302             GROUP BY
303             oid,
304             proname,
305             proretset,
306             pronargdefaults
307             UNION ALL
308             -- For functions with only IN arguments,
309             -- proargmodes IS NULL
310             SELECT
311             pg_catalog.pg_proc.oid,
312             pg_catalog.pg_proc.proname,
313             pg_catalog.pg_proc.proretset,
314             pg_catalog.pg_proc.pronargdefaults,
315             pg_catalog.pg_proc.proargnames
316             FROM pg_catalog.pg_proc
317             INNER JOIN pg_catalog.pg_namespace ON (pg_catalog.pg_namespace.oid = pg_catalog.pg_proc.pronamespace)
318             WHERE (?::name IS NULL OR pg_catalog.pg_namespace.nspname = ?::name)
319             AND pg_catalog.pg_proc.proname = ?::name
320             AND pg_catalog.pg_proc.proargnames IS NOT NULL
321             AND pg_catalog.pg_proc.proargmodes IS NULL
322             )
323             -- Find any function matching the name
324             -- and having identical argument names
325             SELECT * FROM OnlyINandINOUTArguments
326             WHERE ?::text[] <@ proargnames AND ((
327             -- No default arguments
328             pronargdefaults = 0 AND ?::text[] @> proargnames
329             ) OR (
330             -- Default arguments, only require first input arguments to match
331             pronargdefaults > 0 AND ?::text[] @> proargnames[
332             1
333             :
334             array_upper(proargnames,1) - pronargdefaults
335             ]
336             ))
337             -- The order of arguments doesn't matter,
338             -- so compare the arrays by checking
339             -- if A contains B and B contains A
340             ") or croak "failed to prepare get_proretset query";
341 0 0         $get_proretset->execute($namespace, $namespace, $name, $namespace, $namespace, $name, $argnames, $argnames, $argnames)
342             or croak("failed to execute get_proretset query: " . $get_proretset->errstr);
343             }
344              
345              
346 0           my $proretset;
347 0           my $i = 0;
348 0           while (my $h = $get_proretset->fetchrow_hashref()) {
349 0           $i++;
350 0           $proretset = $h;
351             }
352 0 0         if ($i == 0)
    0          
353             {
354 0           croak "no function matches the input arguments, function: $name";
355             }
356             elsif ($i == 1)
357             {
358             # The function exists and can be called. Add it to the cache if the
359             # caller has asked for caching.
360 0 0         $self->{proretset_cache}->{$cachekey} = $proretset->{proretset} if ($self->{EnableFunctionLookupCache});
361              
362 0           return $proretset->{proretset};
363             }
364             else
365             {
366 0           croak "multiple functions matches the same input arguments, function: $name";
367             }
368             }
369              
370             sub _call
371             {
372 0     0     my ($self,$name,$args,$namespace) = @_;
373              
374 0           my $validate_name_regex = qr/^[a-zA-Z_][a-zA-Z0-9_]*$/;
375              
376 0 0         unless (defined $args)
377             {
378 0           $args = {};
379             }
380              
381 0 0 0       croak "dbh and name must be defined" unless defined $self->{dbh} && defined $name;
382 0 0 0       croak "invalid format of namespace" unless !defined $namespace || $namespace =~ $validate_name_regex;
383 0 0         croak "invalid format of name" unless $name =~ $validate_name_regex;
384 0 0         croak "args must be a hashref" unless ref $args eq 'HASH';
385              
386 0           my @arg_names = sort keys %{$args};
  0            
387 0           my @arg_values = @{$args}{@arg_names};
  0            
388              
389 0           foreach my $arg_name (@arg_names)
390             {
391 0 0         if ($arg_name !~ $validate_name_regex)
392             {
393 0           croak "invalid format of argument name: $arg_name";
394             }
395             }
396              
397 0           my $proretset = $self->_proretset($name, \@arg_names, $namespace);
398              
399 0           my $placeholders = join ",", map { "$_ := ?" } @arg_names;
  0            
400 0 0         my $sql = 'SELECT * FROM ' . (defined $namespace ? "$namespace.$name" : $name) . '(' . $placeholders . ');';
401              
402 0           local $self->{dbh}->{RaiseError} = 0;
403 0           my $query = $self->{dbh}->prepare($sql);
404              
405              
406             # reset the error information
407 0           $self->{SQLState} = '00000';
408 0           $self->{SQLErrorMessage} = undef;
409              
410 0           my $failed = !defined $query->execute(@arg_values);
411              
412             # If something went wrong, we might have to invalidate the cache entry for
413             # this function.
414 0 0 0       if ($failed && $self->{EnableFunctionLookupCache})
415             {
416             # List of SQLSTATEs that warrant cache invalidation. See
417             # _invalidate_proretset_cache_entry() for more information and
418             # http://www.postgresql.org/docs/current/static/errcodes-appendix.html
419             # for a list of error codes.
420             #
421             # Unfortunately there is no way to reliably tell whether our call or
422             # something in the function we called caused the error. However, for
423             # our use case it doesn't really matter since in the worst case that
424             # would only mean unnecessary invalidations for functions that are
425             # already slow to run because they're broken.
426 0           my @sqlstates = (
427             "42883", # undefined function
428             "42725" # ambiguous function
429             );
430              
431 0           $self->_invalidate_proretset_cache_entry($name, \@arg_names, $namespace)
432 0 0         if ((scalar grep { $_ eq $query->state } @sqlstates) > 0);
433             }
434              
435              
436 0 0 0       if ($failed && $self->{RaiseError})
    0          
437             {
438 0           croak "Call to $name failed: $DBI::errstr";
439             }
440             elsif ($failed)
441             {
442             # if we failed but RaiseError wasn't set, let the caller deal with the problem
443 0           $self->{SQLState} = $query->state;
444 0           $self->{SQLErrorMessage} = $query->errstr;
445 0           return undef;
446             }
447              
448 0           my $output;
449             my $num_cols;
450 0           my @output_columns;
451 0           for (my $row_number=0; my $h = $query->fetchrow_hashref(); $row_number++)
452             {
453 0 0         if ($row_number == 0)
454             {
455 0           @output_columns = sort keys %{$h};
  0            
456 0           $num_cols = scalar @output_columns;
457 0 0         croak "no columns in return" unless $num_cols >= 1;
458             }
459 0 0         if ($proretset == 0)
    0          
460             {
461             # single-row
462 0 0         croak "function returned multiple rows" if defined $output;
463 0 0         if ($num_cols == 1)
    0          
464             {
465             # single-column
466 0           $output = $h->{$output_columns[0]};
467             }
468             elsif ($num_cols > 1)
469             {
470             # multi-column
471 0           $output = $h;
472             }
473             }
474             elsif ($proretset == 1)
475             {
476             # multi-row
477 0 0         if ($num_cols == 1)
    0          
478             {
479             # single-column
480 0           push @$output, $h->{$output_columns[0]};
481             }
482             elsif ($num_cols > 1)
483             {
484             # multi-column
485 0           push @$output, $h;
486             }
487             }
488             }
489 0           return $output;
490             }
491              
492             1;
493              
494             =begin Pod::Coverage
495              
496             new
497              
498             =end Pod::Coverage
499              
500             # vim: ts=8:sw=4:sts=4:et