File Coverage

blib/lib/App/Dochazka/REST/Model/Shared.pm
Criterion Covered Total %
statement 32 296 10.8
branch 0 104 0.0
condition 0 29 0.0
subroutine 11 53 20.7
pod 20 20 100.0
total 63 502 12.5


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33              
34             use 5.012;
35 41     41   2720 use strict;
  41         141  
36 41     41   250 use warnings;
  41         86  
  41         819  
37 41     41   182  
  41         92  
  41         1152  
38             use App::CELL qw( $CELL $log $meta $site );
39 41     41   211 use Data::Dumper;
  41         87  
  41         3592  
40 41     41   255 use JSON;
  41         77  
  41         1765  
41 41     41   11729 use Params::Validate qw( :all );
  41         186657  
  41         299  
42 41     41   4417 use Try::Tiny;
  41         463  
  41         5308  
43 41     41   310  
  41         80  
  41         2138  
44              
45              
46              
47             =head1 NAME
48              
49             App::Dochazka::REST::Model::Shared - functions shared by several modules within
50             the data model
51              
52              
53              
54              
55             =head1 SYNOPSIS
56              
57             use App::Dochazka::REST::Model::Shared;
58              
59             ...
60              
61              
62              
63              
64             =head1 EXPORTS
65              
66             =cut
67              
68             use Exporter qw( import );
69 41     41   256 our @EXPORT_OK = qw(
  41         77  
  41         83542  
70             canonicalize_date
71             canonicalize_ts
72             canonicalize_tsrange
73             cud
74             cud_generic
75             decode_schedule_json
76             get_history
77             load
78             load_multiple
79             noof
80             priv_by_eid
81             schedule_by_eid
82             select_single
83             select_set_of_single_scalar_rows
84             split_tsrange
85             timestamp_delta_minus
86             timestamp_delta_plus
87             tsrange_intersection
88             tsrange_equal
89             );
90              
91              
92              
93              
94             =head1 FUNCTIONS
95              
96              
97             =head2 canonicalize_date
98              
99             Given a string that PostgreSQL might recognize as a date, pass it to
100             the database via the SQL statement:
101              
102             SELECT CAST( ? AS date )
103              
104             and return the resulting status object.
105              
106             =cut
107              
108             my ( $conn, $ts ) = @_;
109              
110 0     0 1 0 my $status = select_single(
111             conn => $conn,
112 0         0 sql => 'SELECT CAST( ? AS date )',
113             keys => [ $ts ],
114             );
115             _replace_payload_array_with_string( $status ) if $status->ok;
116             return $status;
117 0 0       0 }
118 0         0  
119              
120             =head2 canonicalize_ts
121              
122             Given a string that might be a timestamp, "canonicalize" it by running it
123             through the database in the SQL statement:
124              
125             SELECT CAST( ? AS timestamptz )
126              
127             =cut
128              
129             my ( $conn, $ts ) = @_;
130              
131             my $status = select_single(
132 0     0 1 0 conn => $conn,
133             sql => 'SELECT CAST( ? AS timestamptz )',
134 0         0 keys => [ $ts ],
135             );
136             _replace_payload_array_with_string( $status ) if $status->ok;
137             return $status;
138             }
139 0 0       0  
140 0         0 my $status = shift;
141             $status->payload( $status->payload->[0] );
142             return $status;
143             }
144 0     0   0  
145 0         0  
146 0         0 =head2 canonicalize_tsrange
147              
148             Given a string that might be a tsrange, "canonicalize" it by running it
149             through the database in the SQL statement:
150              
151             SELECT CAST( ? AS tstzrange )
152              
153             Returns an L<App::CELL::Status> object. If the status code is OK, then the
154             tsrange is OK and its canonicalized form is in the payload. Otherwise, some
155             kind of error occurred, as described in the status object.
156              
157             =cut
158              
159             my ( $conn, $tsr ) = @_;
160              
161             my $status = select_single(
162             conn => $conn,
163             sql => 'SELECT CAST( ? AS tstzrange)',
164 0     0 1 0 keys => [ $tsr ],
165             );
166 0         0 _replace_payload_array_with_string( $status ) if $status->ok;
167             return $CELL->status_err( 'DOCHAZKA_TSRANGE_EMPTY' ) if $status->ok and $status->payload eq "empty";
168             return $status;
169             }
170              
171 0 0       0  
172 0 0 0     0 =head2 cud
173 0         0  
174             Attempts to Create, Update, or Delete a single database record. Takes the
175             following PARAMHASH:
176              
177             =over
178              
179             =item * conn
180              
181             The L<DBIx::Connector> object with which to gain access to the database.
182              
183             =item * eid
184              
185             The EID of the employee originating the request (needed for the audit triggers).
186              
187             =item * object
188              
189             The Dochazka datamodel object to be worked on.
190              
191             =item * sql
192              
193             The SQL statement to execute (should be INSERT, UPDATE, or DELETE).
194              
195             =item * attrs
196              
197             An array reference containing the bind values to be plugged into the SQL
198             statement.
199              
200             =back
201              
202             Returns a status object.
203              
204             Important note: it is up to the programmer to not pass any SQL statement that
205             might affect more than one record.
206              
207             =cut
208              
209             my %ARGS = validate( @_, {
210             conn => { isa => 'DBIx::Connector' },
211             eid => { type => SCALAR },
212             object => { can => [ qw( insert delete ) ] },
213             sql => { type => SCALAR },
214             attrs => { type => ARRAYREF }, # order of attrs must match SQL statement
215 0     0 1 0 } );
216              
217             my ( $status, $rv, $count );
218              
219             try {
220             local $SIG{__WARN__} = sub {
221             die @_;
222             };
223 0         0  
224             # start transaction
225             $ARGS{'conn'}->txn( fixup => sub {
226              
227 0         0 # get DBI db handle
228 0     0   0 my $dbh = shift;
229              
230             # set the dochazka.eid GUC session parameter
231             $dbh->do( $site->SQL_SET_DOCHAZKA_EID_GUC, undef, ( $ARGS{'eid'}+0 ) );
232              
233             # prepare the SQL statement and bind parameters
234 0         0 my $sth = $dbh->prepare( $ARGS{'sql'} );
235             my $counter = 0;
236             map {
237 0         0 $counter += 1;
238             $sth->bind_param( $counter, $ARGS{'object'}->{$_} );
239             } @{ $ARGS{'attrs'} };
240 0         0  
241 0         0 # execute the SQL statement
242             $rv = $sth->execute;
243 0         0 $log->debug( "cud: DBI execute returned " . Dumper( $rv ) );
244 0         0 if ( $rv == 1 ) {
245 0         0  
  0         0  
246             # a record was returned; get the values
247             my $rh = $sth->fetchrow_hashref;
248 0         0 $log->info( "Statement " . $sth->{'Statement'} . " RETURNING values: " . Dumper( $rh ) );
249 0         0 # populate object with all RETURNING fields
250 0 0       0 map { $ARGS{'object'}->{$_} = $rh->{$_}; } ( keys %$rh );
    0          
    0          
    0          
251              
252             # count number of rows affected
253 0         0 $count = $sth->rows;
254 0         0  
255             } elsif ( $rv eq '0E0' ) {
256 0         0  
  0         0  
257             # no error, but no record returned either
258             $count = $sth->rows;
259 0         0  
260             } elsif ( $rv > 1 ) {
261             $status = $CELL->status_crit(
262             'DOCHAZKA_CUD_MORE_THAN_ONE_RECORD_AFFECTED',
263             args => [ $sth->{'Statement'} ]
264 0         0 );
265             } elsif ( $rv == -1 ) {
266             $status = $CELL->status_err(
267             'DOCHAZKA_CUD_UNKNOWN_NUMBER_OF_RECORDS_AFFECTED',
268             args => [ $sth->{'Statement'} ]
269 0         0 );
270             } else {
271             $status = $CELL->status_crit( 'DOCHAZKA_DBI_EXECUTE_WEIRDNESS' );
272             }
273             } );
274 0         0 } catch {
275             my $errmsg = $_;
276             if ( not defined( $errmsg ) ) {
277 0         0 $log->err( '$_ undefined in catch' );
278             $errmsg = '<NONE>';
279 0         0 }
280             if ( ! $site->DOCHAZKA_SQL_TRACE ) {
281 0     0   0 $errmsg =~ s/^DBD::Pg::st execute failed: //;
282 0 0       0 $errmsg =~ s#at /usr/lib/perl5/.* line .*\.$##;
283 0         0 }
284 0         0 if ( ! defined( $status ) ) {
285             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR',
286 0 0       0 args => [ $errmsg ],
287 0         0 DBI_return_value => $rv,
288 0         0 );
289             }
290 0 0       0 };
291 0         0  
292             if ( not defined( $status ) ) {
293             $status = $CELL->status_ok( 'DOCHAZKA_CUD_OK',
294             DBI_return_value => $rv,
295             payload => $ARGS{'object'},
296 0         0 count => $count,
297             );
298 0 0       0 }
299              
300             return $status;
301 0         0 }
302              
303              
304             =head2 cud_generic
305              
306 0         0 Attempts to execute a generic Create, Update, or Delete database operation.
307             Takes the following PARAMHASH:
308              
309             =over
310              
311             =item * conn
312              
313             The L<DBIx::Connector> object with which to gain access to the database.
314              
315             =item * eid
316              
317             The EID of the employee originating the request (needed for the audit triggers).
318              
319             =item * sql
320              
321             The SQL statement to execute (should be INSERT, UPDATE, or DELETE).
322              
323             =item * bind_params
324              
325             An array reference containing the bind values to be plugged into the SQL
326             statement.
327              
328             =back
329              
330             Returns a status object.
331              
332             Important note: it is up to the programmer to not pass any SQL statement that
333             might affect more than one record.
334              
335             =cut
336              
337             my %ARGS = validate( @_, {
338             conn => { isa => 'DBIx::Connector' },
339             eid => { type => SCALAR },
340             sql => { type => SCALAR },
341             bind_params => { type => ARRAYREF, optional => 1 }, # order must match SQL statement
342             } );
343             $log->info( "Entering " . __PACKAGE__ . "::cud_generic with" );
344 0     0 1 0 $log->info( "sql: $ARGS{sql}" );
345             $log->info( "bind_param: " . Dumper( $ARGS{bind_params} ) );
346              
347             my ( $status, $rv, $count );
348              
349             try {
350 0         0 local $SIG{__WARN__} = sub {
351 0         0 die @_;
352 0         0 };
353              
354 0         0 # start transaction
355             $ARGS{'conn'}->txn( fixup => sub {
356              
357             # get DBI db handle
358 0         0 my $dbh = shift;
359 0     0   0  
360             # set the dochazka.eid GUC session parameter
361             $dbh->do( $site->SQL_SET_DOCHAZKA_EID_GUC, undef, ( $ARGS{'eid'}+0 ) );
362              
363             # prepare the SQL statement and bind parameters
364             my $sth = $dbh->prepare( $ARGS{'sql'} );
365 0         0 my $counter = 0;
366             map {
367             $counter += 1;
368 0         0 $sth->bind_param( $counter, $_ || undef );
369             } @{ $ARGS{'bind_params'} };
370              
371 0         0 # execute the SQL statement
372 0         0 $rv = $sth->execute;
373             $log->debug( "cud_generic: DBI execute returned " . Dumper( $rv ) );
374 0         0 if ( $rv >= 1 ) {
375 0   0     0  
376 0         0 # count number of rows affected
  0         0  
377             $count = $sth->rows;
378              
379 0         0 } elsif ( $rv eq '0E0' ) {
380 0         0  
381 0 0       0 # no error, but no record returned either
    0          
    0          
382             $count = $sth->rows;
383              
384 0         0 } elsif ( $rv == -1 ) {
385             $status = $CELL->status_err(
386             'DOCHAZKA_CUD_UNKNOWN_NUMBER_OF_RECORDS_AFFECTED',
387             args => [ $sth->{'Statement'} ]
388             );
389 0         0 } else {
390             $status = $CELL->status_crit( 'DOCHAZKA_DBI_EXECUTE_WEIRDNESS' );
391             }
392             } );
393             } catch {
394 0         0 my $errmsg = $_;
395             if ( not defined( $errmsg ) ) {
396             $log->err( '$_ undefined in catch' );
397 0         0 $errmsg = '<NONE>';
398             }
399 0         0 if ( not defined( $status ) ) {
400             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR',
401 0     0   0 args => [ $errmsg ],
402 0 0       0 DBI_return_value => $rv,
403 0         0 );
404 0         0 }
405             };
406 0 0       0  
407 0         0 if ( not defined( $status ) ) {
408             $status = $CELL->status_ok( 'DOCHAZKA_CUD_OK',
409             DBI_return_value => $rv,
410             count => $count,
411             );
412 0         0 }
413              
414 0 0       0 return $status;
415 0         0 }
416              
417              
418             =head2 decode_schedule_json
419              
420             Given JSON string representation of the schedule, return corresponding HASHREF.
421 0         0  
422             =cut
423              
424             my ( $json_str ) = @_;
425              
426             return unless $json_str;
427             return JSON->new->utf8->canonical(1)->decode( $json_str );
428             }
429              
430              
431             =head2 get_history
432 0     0 1 0  
433             This function takes a number of arguments. The first two are (1) a SCALAR
434 0 0       0 argument, which can be either 'priv' or 'schedule', and (2) a L<DBIx::Connector>
435 0         0 object.
436              
437             Following these there is a PARAMHASH which can have one or more of the
438             properties 'eid', 'nick', and 'tsrange'. At least one of { 'eid', 'nick' } must
439             be specified. If both are specified, the employee is determined according to
440             'eid'.
441              
442             The function returns the history of privilege level or schedule changes for
443             that employee over the given tsrange, or the entire history if no tsrange is
444             supplied.
445              
446             The return value will always be an L<App::CELL::Status|status> object.
447              
448             Upon success, the payload will be a reference to an array of history
449             objects. If nothing is found, the array will be empty. If there is a DBI error,
450             the payload will be undefined.
451              
452             =cut
453              
454             my $t = shift; # 'priv' or 'sched'
455             my $conn = shift;
456             validate_pos( @_, 1, 1, 0, 0, 0, 0 );
457             my %ARGS = validate( @_, {
458             eid => { type => SCALAR, optional => 1 },
459             nick => { type => SCALAR, optional => 1 },
460             tsrange => { type => SCALAR|UNDEF, optional => 1 },
461             } );
462              
463 0     0 1 0 $log->debug("Entering get_history for $t - arguments: " . Dumper( \%ARGS ) );
464 0         0  
465 0         0 my ( $sql, $sk, $status, $result, $tsr );
466 0         0 if ( exists $ARGS{'nick'} ) {
467             $sql = ($t eq 'priv')
468             ? $site->SQL_PRIVHISTORY_SELECT_RANGE_BY_NICK
469             : $site->SQL_SCHEDHISTORY_SELECT_RANGE_BY_NICK;
470             $result->{'nick'} = $ARGS{'nick'};
471             $result->{'eid'} = $ARGS{'eid'} if exists $ARGS{'eid'};
472 0         0 $sk = $ARGS{'nick'};
473             }
474 0         0 if ( exists $ARGS{'eid'} ) {
475 0 0       0 $sql = ($t eq 'priv')
476 0 0       0 ? $site->SQL_PRIVHISTORY_SELECT_RANGE_BY_EID
477             : $site->SQL_SCHEDHISTORY_SELECT_RANGE_BY_EID;
478             $result->{'eid'} = $ARGS{'eid'};
479 0         0 $result->{'nick'} = $ARGS{'nick'} if exists $ARGS{'nick'};
480 0 0       0 $sk = $ARGS{'eid'};
481 0         0 }
482             $log->debug("sql == $sql");
483 0 0       0 $tsr = ( $ARGS{'tsrange'} )
484 0 0       0 ? $ARGS{'tsrange'}
485             : '[,)';
486             $result->{'tsrange'} = $tsr;
487 0         0 $log->debug("tsrange == $tsr");
488 0 0       0  
489 0         0 die "AAAAAAAAAAAHHHHH! Engulfed by the abyss" unless $sk and $sql and $tsr;
490              
491 0         0 $result->{'history'} = [];
492             try {
493 0 0       0 $conn->run( fixup => sub {
494             my $sth = $_->prepare( $sql );
495 0         0 $sth->execute( $sk, $tsr );
496 0         0 while( defined( my $tmpres = $sth->fetchrow_hashref() ) ) {
497             push @{ $result->{'history'} }, $tmpres;
498 0 0 0     0 }
      0        
499             } );
500 0         0 } catch {
501             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
502             };
503 0         0 return $status if defined $status;
504 0         0  
505 0         0 my $counter = scalar @{ $result->{'history'} };
506 0         0 return ( $counter )
  0         0  
507             ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
508 0     0   0 args => [ $counter ], payload => $result, count => $counter )
509             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND',
510 0     0   0 payload => $result, count => $counter );
511 0         0 }
512 0 0       0  
513              
514 0         0 =head2 load
  0         0  
515 0 0       0  
516             Load a database record into an object based on an SQL statement and a set of
517             search keys. The search key must be an exact match: this function returns only
518             1 or 0 records. Call, e.g., like this:
519              
520             my $status = load(
521             conn => $conn,
522             class => __PACKAGE__,
523             sql => $site->DOCHAZKA_SQL_SOME_STATEMENT,
524             keys => [ 44 ]
525             );
526              
527             The status object will be one of the following:
528              
529             =over
530              
531             =item * 1 record found
532              
533             Level C<OK>, code C<DISPATCH_RECORDS_FOUND>, payload: object of type 'class'
534              
535             =item * 0 records found
536              
537             Level C<NOTICE>, code C<DISPATCH_NO_RECORDS_FOUND>, payload: none
538              
539             =item * Database error
540              
541             Level C<ERR>, code C<DOCHAZKA_DBI_ERR>, text: error message, payload: none
542              
543             =back
544              
545             =cut
546              
547             # get and verify arguments
548             my %ARGS = validate( @_, {
549             conn => { isa => 'DBIx::Connector' },
550             class => { type => SCALAR },
551             sql => { type => SCALAR },
552             keys => { type => ARRAYREF },
553             } );
554              
555             # consult the database; N.B. - select may only return a single record
556             my ( $hr, $status );
557             try {
558 0     0 1 0 $ARGS{'conn'}->run( fixup => sub {
559             $hr = $_->selectrow_hashref( $ARGS{'sql'}, undef, @{ $ARGS{'keys'} } );
560             } );
561             } catch {
562             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
563             };
564              
565             # report the result
566 0         0 return $status if $status;
567             return $CELL->status_ok( 'DISPATCH_RECORDS_FOUND', args => [ '1' ],
568             payload => $ARGS{'class'}->spawn( %$hr ), count => 1 ) if defined $hr;
569 0         0 return $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND', count => 0 );
  0         0  
570 0     0   0 }
571              
572 0     0   0  
573 0         0 =head2 load_multiple
574              
575             Load multiple database records based on an SQL statement and a set of search
576 0 0       0 keys. Example:
577              
578 0 0       0 my $status = load_multiple(
579 0         0 conn => $conn,
580             class => __PACKAGE__,
581             sql => $site->DOCHAZKA_SQL_SOME_STATEMENT,
582             keys => [ 'rom%' ]
583             );
584              
585             The return value will be a status object, the payload of which will be an
586             arrayref containing a set of objects. The objects are constructed by calling
587             $ARGS{'class'}->spawn
588              
589             For convenience, a 'count' property will be included in the status object.
590              
591             =cut
592              
593             # get and verify arguments
594             my %ARGS = validate( @_, {
595             conn => { isa => 'DBIx::Connector' },
596             class => { type => SCALAR },
597             sql => { type => SCALAR },
598             keys => { type => ARRAYREF },
599             } );
600             $log->debug( "Entering " . __PACKAGE__ . "::load_multiple" );
601              
602             my $status;
603             my $results = [];
604             try {
605 0     0 1 0 $ARGS{'conn'}->run( fixup => sub {
606             my $sth = $_->prepare( $ARGS{'sql'} );
607             my $bc = 0;
608             map {
609             $bc += 1;
610             $sth->bind_param( $bc, $_ || undef );
611 0         0 } @{ $ARGS{'keys'} };
612             $sth->execute();
613 0         0 # assuming they are objects, spawn them and push them onto @results
614 0         0 while( defined( my $tmpres = $sth->fetchrow_hashref() ) ) {
615             push @$results, $ARGS{'class'}->spawn( %$tmpres );
616             }
617 0         0 } );
618 0         0 } catch {
619             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
620 0         0 };
621 0   0     0 return $status if defined $status;
622 0         0  
  0         0  
623 0         0 my $counter = scalar @$results;
624             $status = ( $counter )
625 0         0 ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
626 0         0 args => [ $counter ], payload => $results, count => $counter, keys => $ARGS{'keys'} )
627             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND',
628 0     0   0 payload => $results, count => $counter );
629             #$log->debug( Dumper $status );
630 0     0   0 return $status;
631 0         0 }
632 0 0       0  
633              
634 0         0 =head2 make_test_exists
635              
636             Returns coderef for a function, 'test_exists', that performs a simple
637 0 0       0 true/false check for existence of a record matching a scalar search key. The
638             record must be an exact match (no wildcards).
639              
640             Takes one argument: a type string C<$t> which is concatenated with the string
641 0         0 'load_by_' to arrive at the name of the function to be called to execute the
642             search.
643              
644             The returned function takes a single argument: the search key (a scalar value).
645             If a record matching the search key is found, the corresponding object
646             (i.e. a true value) is returned. If such a record does not exist, 'undef' (a
647             false value) is returned. If there is a DBI error, the error text is logged
648             and undef is returned.
649              
650             =cut
651              
652              
653             my ( $t ) = validate_pos( @_, { type => SCALAR } );
654             my $pkg = (caller)[0];
655              
656             return sub {
657             my ( $conn, $s_key ) = @_;
658             require Try::Tiny;
659             my $routine = "load_by_$t";
660             my ( $status, $txt );
661             $log->debug( "Entered $t" . "_exists with search key $s_key" );
662             try {
663             no strict 'refs';
664             $status = $pkg->$routine( $conn, $s_key );
665 410     410 1 5104 } catch {
666 410         1993 $txt = "Function " . $pkg . "::test_exists was generated with argument $t, " .
667             "so it tried to call $routine, resulting in exception $_";
668             $status = $CELL->status_crit( $txt );
669 0     0     };
670 0           if ( ! defined( $status ) or $status->level eq 'CRIT' ) {
671 0           die $txt;
672 0           }
673 0           #$log->debug( "Status is " . Dumper( $status ) );
674             return $status->payload if $status->ok;
675 41     41   427 return;
  41         158  
  41         62484  
676 0     0     }
677             }
678 0     0      
679              
680 0           =head2 noof
681 0            
682 0 0 0       Given a L<DBIx::Connector> object and the name of a data model table, returns
683 0           the total number of records in the table.
684              
685             activities employees intervals locks privhistory schedhistory
686 0 0         schedintvls schedules tempintvls
687 0            
688             On failure, returns undef.
689 410         2739  
690             =cut
691              
692             my ( $conn, $table ) = validate_pos( @_,
693             { isa => 'DBIx::Connector' },
694             { type => SCALAR }
695             );
696              
697             return unless grep { $table eq $_; } qw( activities employees intervals locks
698             privhistory schedhistory schedintvls schedules tempintvls );
699              
700             my $count;
701             try {
702             $conn->run( fixup => sub {
703             ( $count ) = $_->selectrow_array( "SELECT count(*) FROM $table" );
704             } );
705 0     0 1   } catch {
706             $CELL->status_crit( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
707             };
708             return $count;
709             }
710 0 0          
  0            
711              
712             =head2 priv_by_eid
713 0            
714             Given an EID, and, optionally, a timestamp, returns the employee's priv
715             level as of that timestamp, or as of "now" if no timestamp was given. The
716 0           priv level will default to 'passerby' if it can't be determined from the
717 0     0     database.
718              
719 0     0     =cut
720 0            
721 0           my ( $conn, $eid, $ts ) = validate_pos( @_,
722             { isa => 'DBIx::Connector' },
723             { type => SCALAR },
724             { type => SCALAR|UNDEF, optional => 1 }
725             );
726             #$log->debug( "priv_by_eid: EID is " . (defined( $eid ) ? $eid : 'undef') . " - called from " . (caller)[1] . " line " . (caller)[2] );
727             return _st_by_eid( $conn, 'priv', $eid, $ts );
728             }
729              
730              
731             =head2 schedule_by_eid
732              
733             Given an EID, and, optionally, a timestamp, returns the SID of the employee's
734             schedule as of that timestamp, or as of "now" if no timestamp was given.
735 0     0 1    
736             =cut
737              
738             my ( $conn, $eid, $ts ) = validate_pos( @_,
739             { isa => 'DBIx::Connector' },
740             { type => SCALAR },
741 0           { type => SCALAR|UNDEF, optional => 1 },
742             );
743             return _st_by_eid( $conn, 'schedule', $eid, $ts );
744             }
745              
746              
747             =head3 _st_by_eid
748              
749             Function that 'priv_by_eid' and 'schedule_by_eid' are wrappers of.
750              
751             =cut
752              
753 0     0 1   my ( $conn, $st, $eid, $ts ) = @_;
754             my ( @args, $sql, $row );
755             $log->debug( "Entering _st_by_eid with \$st == $st, \$eid == $eid, \$ts == " . ( $ts || '<NONE>' ) );
756             if ( $ts ) {
757             # timestamp given
758 0           if ( $st eq 'priv' ) {
759             $sql = $site->SQL_EMPLOYEE_PRIV_AT_TIMESTAMP;
760             } elsif ( $st eq 'schedule' ) {
761             $sql = $site->SQL_EMPLOYEE_SCHEDULE_AT_TIMESTAMP;
762             }
763             @args = ( $sql, undef, $eid, $ts );
764             } else {
765             # no timestamp given
766             if ( $st eq 'priv' ) {
767             $sql = $site->SQL_EMPLOYEE_CURRENT_PRIV;
768             } elsif ( $st eq 'schedule' ) {
769 0     0     $sql = $site->SQL_EMPLOYEE_CURRENT_SCHEDULE;
770 0           }
771 0   0       @args = ( $sql, undef, $eid );
772 0 0         }
773              
774 0 0         $log->debug("About to run SQL statement $sql with parameter $eid - " .
    0          
775 0           " called from " . (caller)[1] . " line " . (caller)[2] );
776              
777 0           my $status;
778             try {
779 0           $conn->run( fixup => sub {
780             ( $row ) = $_->selectrow_array( @args );
781             } );
782 0 0         } catch {
    0          
783 0           $log->debug( 'Encountered DBI error' );
784             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
785 0           };
786             return $status if $status;
787 0            
788             $log->debug( "_st_by_eid success; returning payload " . Dumper( $row ) );
789             return $row;
790 0           }
791              
792              
793 0           =head2 select_single
794              
795             Given a L<DBIx::Connector> object in the 'conn' property, a SELECT statement in
796 0           the 'sql' property and, in the 'keys' property, an arrayref containing a list
797 0     0     of scalar values to plug into the SELECT statement, run a C<selectrow_array>
798             and return the resulting list.
799 0     0      
800 0           Returns a standard status object (see C<load> routine, above, for description).
801 0            
802 0 0         =cut
803              
804 0           my %ARGS = validate( @_, {
805 0           conn => { isa => 'DBIx::Connector' },
806             sql => { type => SCALAR },
807             keys => { type => ARRAYREF },
808             } );
809             my ( $status, @results );
810             $log->info( "select_single keys: " . Dumper( $ARGS{keys} ) );
811             try {
812             $ARGS{'conn'}->run( fixup => sub {
813             @results = $_->selectrow_array( $ARGS{'sql'}, undef, @{ $ARGS{'keys'} } );
814             } );
815             my $count = scalar( @results ) ? 1 : 0;
816             $log->info( "count: $count" );
817             $status = ( $count )
818             ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
819             args => [ $count ], count => $count, payload => \@results )
820             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND' );
821 0     0 1   } catch {
822             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
823             };
824             die "AAAAHAHAAHAAAAAAGGGH! " . __PACKAGE__ . "::select_single" unless $status;
825             return $status;
826 0           }
827 0            
828              
829             =head2 select_set_of_single_scalar_rows
830 0            
  0            
831 0     0     Given DBIx::Connector object, an SQL statement, and a set of keys to bind
832 0 0         into the SQL statement, assume that the statement can return 0-n records
833 0           and that each record consists of a single field that must fit into a single
834 0 0         scalar value.
835              
836             =cut
837              
838             my %ARGS = validate( @_, {
839 0     0     conn => { isa => 'DBIx::Connector' },
840 0           sql => { type => SCALAR },
841 0 0         keys => { type => ARRAYREF },
842 0           } );
843             $log->debug( "Entering " . __PACKAGE__ . "::select_set_of_single_scalar_rows with
844             paramhash " . Dumper( \%ARGS ) );
845              
846             my ( $status, $result_set );
847             try {
848             $ARGS{'conn'}->run( fixup => sub {
849             my $sth = $_->prepare( $ARGS{'sql'} );
850             my $bc = 0;
851             map {
852             $bc += 1;
853             $sth->bind_param( $bc, $_ || undef );
854             } @{ $ARGS{'keys'} };
855             $sth->execute();
856 0     0 1   # push results onto $nicks
857             while( defined( my $tmpres = $sth->fetchrow_arrayref() ) ) {
858             push @$result_set, @$tmpres;
859             }
860             } );
861 0           } catch {
862             $log->debug( 'Encountered DBI error' );
863             $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
864 0           };
865              
866             return $status if $status;
867 0           return $CELL->status_ok( 'RESULT_SET', payload => $result_set );
868 0           }
869              
870 0            
871 0   0       =head2 split_tsrange
872 0            
  0            
873 0           Given a string that might be a tsrange, run it through the database
874             using the SQL statement:
875 0            
876 0           SELECT lower(CAST( ? AS tstzrange )), upper(CAST( ? AS tstzrange ))
877              
878 0     0     If all goes well, the result will be an array ( from, to ) of two
879             timestamps.
880 0     0      
881 0           Returns a status object.
882 0            
883             =cut
884 0 0          
885 0           my ( $conn, $tsr ) = @_;
886              
887             my $status = select_single(
888             conn => $conn,
889             sql => 'SELECT lower(CAST( ? AS tstzrange )), upper(CAST( ? AS tstzrange ))',
890             keys => [ $tsr, $tsr ],
891             );
892             return $status unless $status->ok;
893             my ( $lower, $upper ) = @{ $status->payload };
894             return $CELL->status_err( 'DOCHAZKA_UNBOUNDED_TSRANGE' ) unless defined( $lower ) and
895             defined( $upper ) and $lower ne 'infinity' and $upper ne 'infinity';
896             return $status;
897             }
898              
899              
900             =head2 timestamp_delta_minus
901              
902             Given a timestamp string and an interval string (e.g. "1 week 3 days" ),
903             subtract the interval from the timestamp.
904 0     0 1    
905             Returns a status object. If the database operation is successful, the payload
906 0           will contain the resulting timestamp.
907              
908             =cut
909              
910             my ( $conn, $ts, $delta ) = validate_pos( @_,
911 0 0         { isa => 'DBIx::Connector' },
912 0           { type => SCALAR },
  0            
913 0 0 0       { type => SCALAR },
      0        
      0        
914             );
915 0           $log->info( "timestamp_delta_minus: timestamp $ts, delta $delta" );
916             my $status = select_single(
917             conn => $conn,
918             sql => "SELECT CAST( ? AS timestamptz ) - CAST( ? AS interval )",
919             keys => [ $ts, $delta ],
920             );
921             if ( $status->ok ) {
922             my ( $result ) = @{ $status->payload };
923             return $CELL->status_ok( 'SUCCESS', payload => $result );
924             }
925             return $status;
926             }
927              
928              
929             =head2 timestamp_delta_plus
930 0     0 1    
931             Given a timestamp string and an interval string (e.g. "1 week 3 days" ),
932             add the interval to the timestamp.
933              
934             Returns a status object. If the database operation is successful, the payload
935 0           will contain the resulting timestamp.
936 0            
937             =cut
938              
939             my ( $conn, $ts, $delta ) = validate_pos( @_,
940             { isa => 'DBIx::Connector' },
941 0 0         { type => SCALAR },
942 0           { type => SCALAR },
  0            
943 0           );
944             $log->info( "timestamp_delta_plus: timestamp $ts, delta $delta" );
945 0           my $status = select_single(
946             conn => $conn,
947             sql => "SELECT CAST( ? AS timestamptz ) + CAST( ? AS interval )",
948             keys => [ $ts, $delta ],
949             );
950             if ( $status->ok ) {
951             my ( $result ) = @{ $status->payload };
952             return $CELL->status_ok( 'SUCCESS', payload => $result );
953             }
954             return $status;
955             }
956              
957              
958             =head2 tsrange_intersection
959              
960 0     0 1   Given two strings that might be tsranges, consult the database and return
961             the result of tsrange1 * tsrange2 (also a tsrange).
962              
963             =cut
964              
965 0           my ( $conn, $tr1, $tr2 ) = @_;
966 0            
967             my $status = select_single(
968             conn => $conn,
969             sql => 'SELECT CAST( ? AS tstzrange) * CAST( ? AS tstzrange )',
970             keys => [ $tr1, $tr2 ],
971 0 0         );
972 0           die $status->text unless $status->ok;
  0            
973 0           return $status->payload->[0];
974             }
975 0            
976              
977             =head2 tsrange_equal
978              
979             Given two strings that might be equal tsranges, consult the database and return
980             the result (true or false).
981              
982             =cut
983              
984             my ( $conn, $tr1, $tr2 ) = @_;
985              
986             my $status = select_single(
987 0     0 1   conn => $conn,
988             sql => 'SELECT CAST( ? AS tstzrange) = CAST( ? AS tstzrange )',
989 0           keys => [ $tr1, $tr2 ],
990             );
991             die $status->text unless $status->ok;
992             return $status->payload->[0];
993             }
994 0 0          
995 0            
996              
997             =head1 AUTHOR
998              
999             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
1000              
1001             =cut
1002              
1003             1;
1004