File Coverage

blib/lib/Collection/AutoSQL.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Collection::AutoSQL;
2              
3             =head1 NAME
4              
5             Collection::AutoSQL - class for collections of data, stored in database.
6              
7             =head1 SYNOPSIS
8              
9             use Collection::AutoSQL;
10             my $metaobj = new Collection::AutoSQL::
11             dbh => $dbh, #database connect
12             table => 'metadata', #table name
13             field=> 'mid', #key field (IDs), usually primary,autoincrement
14             cut_key =>1, #delete field mid from readed records,
15             #or delete_key=>1
16             sub_ref =>
17             #callback for create objects for readed records
18             sub { my $id = shift; new MyObject:: shift }
19            
20             dbtype => 'pg' # set type of DataBase to PostgreSQL (default: mysql)
21              
22             =head1 DESCRIPTION
23              
24             Provide simply access to records, with unique field.
25              
26             For exampe:
27              
28             HAVE mysql table:
29              
30             mysql> \u orders
31             mysql> select * from beers;
32             +-----+--------+-----------+
33             | bid | bcount | bname |
34             +-----+--------+-----------+
35             | 1 | 1 | heineken |
36             | 2 | 1 | broadside |
37             | 3 | 2 | tiger |
38             | 4 | 2 | castel |
39             | 5 | 3 | karhu |
40             +-----+--------+-----------+
41             5 rows in set (0.00 sec)
42              
43             my $beers = new Collection::AutoSQL::
44             dbh => $dbh, #database connect
45             table => 'beers', #table name
46             field => 'bid', #key field (IDs), usually primary,autoincrement
47             cut_key => 1; #delete field 'bid' from readed records,
48              
49              
50             my $heineken = $beers->fetch_one(1);
51             #SELECT * FROM beers WHERE bid in (1)
52              
53             print Dumper($heineken);
54              
55             ...
56              
57             $VAR1 = {
58             'bcount' => '1',
59             'bname' => 'heineken'
60             };
61             ...
62            
63             $heineken->{bcount}++;
64              
65             my $karhu = $beers->fetch(5);
66             #SELECT * FROM beers WHERE bid in (5)
67            
68             $karhu->{bcount}++;
69            
70             $beers->store;
71             #UPDATE beers SET bcount='2',bname='heineken' where bid=1
72             #UPDATE beers SET bcount='4',bname='karhu' where bid=5
73              
74             my $hash = $beers->fetch({bcount=>[4,1]});
75             #SELECT * FROM beers WHERE ( bcount in (4,1) )
76            
77             print Dumper($hash);
78            
79             ...
80              
81             $VAR1 = {
82             '2' => {
83             'bcount' => '1',
84             'bname' => 'broadside'
85             },
86             '5' => {
87             'bcount' => '4',
88             'bname' => 'karhu'
89             }
90             };
91              
92             ...
93              
94              
95              
96             =head1 METHODS
97              
98             =cut
99              
100 1     1   31211 use strict;
  1         4  
  1         46  
101 1     1   7 use warnings;
  1         1  
  1         38  
102 1     1   6 use Data::Dumper;
  1         2  
  1         43  
103 1     1   5 use Carp;
  1         2  
  1         61  
104 1     1   524 use Collection;
  1         3  
  1         38  
105 1     1   6 use Collection::Utl::Base;
  1         2  
  1         44  
106 1     1   6 use Collection::Utl::ActiveRecord;
  1         2  
  1         37  
107 1     1   483 use Collection::Utl::Flow;
  0            
  0            
108             @Collection::AutoSQL::ISA = qw(Collection);
109             $Collection::AutoSQL::VERSION = '1.1';
110             attributes
111             qw( _dbh _table_name _key_field _is_delete_key_field _sub_ref _fields);
112              
113             sub _init {
114             my $self = shift;
115             my %arg = @_;
116             $self->_dbh( $arg{dbh} );
117             $self->_table_name( $arg{table} );
118             $self->_key_field( $arg{field} );
119             $self->_is_delete_key_field( $arg{delete_key} || $arg{cut_key} );
120             $self->_fields( $arg{fields} || {} );
121             $self->_sub_ref( $arg{sub_ref} );
122             $self->SUPER::_init(@_);
123             }
124              
125             =head2 get_dbh
126              
127             Return current $dbh.
128              
129             =cut
130              
131             sub get_dbh {
132             return $_[0]->_dbh;
133             }
134              
135             =head2 get_ids_where()
136              
137             Return ref to ARRAY of readed IDs.
138              
139             =cut
140              
141             sub get_ids_where {
142             my $self = shift;
143             my $where = shift || return [];
144             my $limit = 0;
145             my $dbh = $self->_dbh();
146             my $table_name = $self->_table_name();
147             my $field = $self->_key_field;
148             my $query = "SELECT $field FROM $table_name WHERE $where";
149             if ($limit) {
150             $query .= " limit $limit";
151             }
152             return ( $dbh->selectcol_arrayref($query) || [] );
153             }
154              
155             sub after_load {
156             my $self = shift;
157             return $_[0];
158             }
159              
160             sub before_save {
161             my $self = shift;
162             return $_[0];
163             }
164              
165             sub _query_dbh {
166             my $self = shift;
167             my $query = shift;
168             my $dbh = $self->_dbh;
169             my $sth = $dbh->prepare($query) or croak $dbh::errstr. "\nSQL: $query";
170             $sth->execute(@_) or croak $dbh::errstr. "\nSQL: $query";
171             return $sth;
172             }
173              
174             sub _store {
175             my ( $self, $ref ) = @_;
176             my $table_name = $self->_table_name();
177             my $field = $self->_key_field;
178              
179             while ( my ( $key, $rec_ref ) = each %$ref ) {
180             my $tmp_val = ref($rec_ref) eq 'HASH' ? $rec_ref : $rec_ref->_get_attr;
181             my $prepared = $self->before_save($tmp_val);
182             my @rows = ref($prepared) eq 'ARRAY' ? @$prepared : ($prepared);
183             foreach my $val (@rows) {
184             my @records =
185             map { [ $_, defined( $val->{$_} ) ? $val->{$_} : '' ] }
186             keys %$val;
187             my $query =
188             "UPDATE $table_name SET "
189             . join( ",", map { qq!$_->[0]=\?! } @records )
190             . " where $field=?";
191             $self->_query_dbh( $query, map ( $_->[1], @records ), $key );
192             } #foreach
193             } #while
194             }
195              
196             =head2 _expand_rules ( [, ] )
197              
198             convert array of terms to scructs with type field
199              
200             Got
201             { test => 1, guid => $two },'key'
202              
203             Return array:
204              
205             (
206              
207             {
208             'values' => [1],
209             'term' => '=',
210             'field' => 'test'
211             },
212             {
213             'values' => ['4D56A984-0B5E-11DC-8292-3DE558089BC5'],
214             'term' => '=',
215             'field' => 'guid',
216             'type' => 'varchar'
217             }
218             )
219              
220             =cut
221              
222             sub _expand_rules {
223             my $self = shift;
224             my @res = ();
225             my $field = $self->_key_field;
226              
227             #group { id =>'1221'}, {id=>'212'} to
228             # { field=>[ '1221', '212' ] }
229             my @grouped = ();
230             foreach my $exp (@_) {
231             if ( ref($exp) ) {
232              
233             # convert scalar values to ref
234             for ( values %$exp ) {
235             $_ = [$_] unless ref($_);
236             }
237             push @grouped, $exp;
238             }
239             else {
240              
241             #got key
242             my $last_rec = $grouped[-1];
243              
244             #check if previus element is key value
245             if ( $last_rec
246             and exists $last_rec->{$field}
247             and ( keys(%$last_rec) == 1 ) )
248             {
249             push @{ $last_rec->{$field} }, $exp;
250              
251             }
252             else {
253             push @grouped, { $field => [$exp] };
254             }
255              
256             }
257             }
258              
259             #now convert passed hashes to special structs with type
260             my @result = ();
261             my $fields = $self->_fields;
262             foreach my $rec (@grouped) {
263             my @group = ();
264             while ( my ( $field_name, $values ) = each %$rec ) {
265              
266             #fill term
267             my $term = '='; #default term value
268             #clear fielname from terms
269             if ( $field_name =~ s%([<>])%% ) {
270             $term = $1;
271             }
272             my %rule =
273             ( field => $field_name, 'values' => $values, term => $term );
274              
275             #fill type
276             if ( my $type = $fields->{$field_name} ) {
277             $rule{type} = $type;
278             }
279             push @group, \%rule;
280             }
281             push @result, \@group;
282             }
283             return @result;
284             }
285              
286             =head2 _prepare_where
287              
288             return expression or undef else
289              
290             =cut
291              
292             sub _prepare_where {
293             my $self = shift;
294             my $dbh = $self->_dbh();
295             my $field = $self->_key_field;
296             my @extra_id;
297             my @docs;
298              
299             # group ids and add fill type of fields
300             my @processed = $self->_expand_rules(@_);
301             my @sql_or = ();
302             foreach my $group (@processed) {
303             my @sql_and = ();
304             foreach my $rec (@$group) {
305              
306             my $values = [ @{ $rec->{'values'} } ];
307             my $type = $rec->{'type'};
308             my $term = $rec->{'term'};
309             my $field = $rec->{'field'};
310              
311             #process varchar values
312             if ( defined $type ) {
313             if ( $type eq 'varchar' ) {
314             $_ = $dbh->quote($_) for @$values;
315             }
316             }
317             else {
318             for (@$values) {
319             $_ = $dbh->quote($_) if !/^\d+$/;
320             }
321              
322             }
323              
324             #construct query
325             my $sql_term = $term;
326              
327             #this
328             #
329             # check type and = or like !
330             #
331             #
332             my $values_sql;
333             if ( scalar @$values > 1 ) {
334             $values_sql = "(" . join( ",", @$values ) . ")";
335             $sql_term = "in" if $sql_term eq '=';
336             }
337             else {
338             $values_sql = "@$values";
339             }
340             push @sql_and, "$field $sql_term $values_sql";
341             }
342             push @sql_or, "(" . join( " and ", @sql_and ) . ")" if @sql_and;
343             }
344             return join " or ", @sql_or;
345             }
346              
347             sub _fetch {
348             my $self = shift;
349             my $dbh = $self->_dbh();
350             my $table_name = $self->_table_name();
351             my $field = $self->_key_field;
352             my $where = $self->_prepare_where(@_);
353             return {} unless $where;
354             my $str = "SELECT * FROM $table_name WHERE $where";
355             my $result = {};
356             my %keys_hash;
357             my $qrt = $self->_query_dbh($str);
358              
359             while ( my $rec = $qrt->fetchrow_hashref ) {
360             my %hash = %$rec;
361             my $id = $hash{$field};
362             delete $hash{$field} if $self->_is_delete_key_field;
363             $result->{$id} = $self->after_load( \%hash );
364             }
365             $qrt->finish;
366             return $result;
367             }
368              
369             sub _create {
370             my ( $self, %arg ) = @_;
371             my $table_name = $self->_table_name();
372             my $id;
373             my $field = $self->_key_field;
374             if ( $self->_is_delete_key_field ) {
375             $id = $arg{$field};
376             delete $arg{$field};
377             }
378             my @keys = keys %arg;
379             my $str =
380             "INSERT INTO $table_name ("
381             . join( ",", @keys )
382             . ") VALUES ("
383             . join( ",",
384             map { $self->_dbh()->quote( defined($_) ? $_ : '' ) }
385             map { $arg{$_} } @keys )
386             . ")";
387             $self->_query_dbh($str);
388             my $inserted_id;
389             if ( !$self->_is_delete_key_field && exists $arg{$field} ) {
390             $inserted_id = $arg{$field};
391             }
392             else {
393             $inserted_id =
394             $self->_dbh->last_insert_id( '', '', $table_name, $field )
395             || $self->GetLastID();
396             }
397             return { $inserted_id => $self->fetch_one($inserted_id) };
398             }
399              
400             sub _delete {
401             my $self = shift;
402             my $table_name = $self->_table_name();
403             my $field = $self->_key_field;
404             return [] unless scalar @_;
405             my $str = "DELETE FROM $table_name WHERE $field IN ("
406             . join( ",", qw/?/ x @_ ) . ")";
407             $self->_query_dbh( $str, @_ );
408             return \@_;
409             }
410              
411             sub _fetch_ids {
412             my $self = shift;
413             my $dbh = $self->_dbh();
414             my $table_name = $self->_table_name();
415             my $field = $self->_key_field;
416             my $query = "SELECT $field FROM $table_name";
417             return $dbh->selectcol_arrayref($query);
418             }
419              
420             #__flow_sql__ $sql_query,[values for sql_query], $on_page_count, $page_num
421             sub __flow_sql__ {
422             my $self = shift;
423             my $flow = shift;
424             my $query = shift;
425             my $params = shift; #[array]
426             my $bulk = shift;
427             my $one_page = shift;
428             my $dbh = $self->_dbh();
429             my $field = $self->_key_field;
430             my $page = $one_page || 0;
431             my $count = 0;
432             my $flow_res;
433             do {
434             my $query_limit =
435             ( ($self->{dbtype}|| '' ) eq 'pg')
436             ? "$query limit $bulk offset " . ( $page * $bulk )
437             : "$query limit " . ( $page * $bulk ) . ", $bulk";
438             my $res = $dbh->selectcol_arrayref( $query_limit, {}, @$params );
439             $count = scalar(@$res);
440             $flow_res =
441             $flow->_flow( map { $self->after_load( { $field => $_ } )->{$field} }
442             @$res );
443             $page++;
444              
445             } until $count < $bulk or defined($one_page) or $flow_res;
446             return undef;
447              
448             }
449              
450             =head2 list_ids [ flow=>$Flow],
451              
452             Return list of ids
453              
454              
455             params:
456              
457             flow - Flow:: object for streaming results
458             onpage - [pagination] count of ids on page
459             page - [pagination] requested page ( depend on onpage)
460             exp - ref to expression for select
461             desc - revert sorting ([1,0])
462             where - custom where if needed, instead expr ['where sring', $query_param1,..]
463             query - custom query
464             uniq - set uniq flag ( eq GROUP BY (key) )
465             order - ORDER BY field
466              
467             return:
468             [array] - array of ids
469              
470             if used C param:
471             "string" - if error
472             undef - ok
473              
474             expamles:
475              
476             $c->list_ids() #return [array of ids]
477              
478             $c->list_ids(flow=>$flow, exp=>{ type=>"t1", "date<"=>12341241 },
479             page=>2, onpage=>10, desc=>1 )
480              
481             =cut
482              
483             sub list_ids {
484             my $self = shift;
485             my %args = @_;
486              
487             # return array ref by default
488             return $self->_fetch_ids unless scalar(@_);
489             my @query_param = ();
490             my $where;
491             if ( my $custom_where = $args{'where'} ) {
492             ( $where, @query_param ) = @{$custom_where};
493             }
494             elsif ( my $exp = $args{'expr'} ) {
495             ( $where, @query_param ) = $self->_prepare_where($exp);
496             }
497              
498             #make query
499             my $dbh = $self->_dbh();
500             my $table_name = $self->_table_name();
501             my $field = $self->_key_field;
502             my $query = $args{query} || "SELECT $field FROM $table_name";
503             $query .= " where $where" if $where;
504             if ($args{uniq}) {
505             #strip dups
506             $query .= " group by $field";
507             }
508             my $onpage = $args{onpage} || 10000;
509              
510             #add order by
511             if ( my $orderby = $args{order} ) {
512             $query .= " ORDER BY $orderby";
513             }
514              
515             #change sorting
516             $query .= " DESC" if $args{desc};
517              
518             if ( my $flow = $args{flow} ) {
519             my $fparser = $flow->parser;
520             $fparser->begin;
521             $self->__flow_sql__( $fparser, $query, \@query_param, $onpage,
522             $args{page} );
523             $fparser->end;
524             } else {
525             #return flow
526             new Collection::Utl::Flow:: __flow_sql__=>[
527             $query, \@query_param, $onpage,
528             $args{page}],
529             __collection__ => $self
530             }
531             }
532              
533             sub _prepare_record {
534             my ( $self, $key, $ref ) = @_;
535             my %hash;
536             tie %hash, 'Collection::Utl::ActiveRecord', hash => $ref;
537             if ( ref( $self->_sub_ref ) eq 'CODE' ) {
538             return $self->_sub_ref()->( $key, \%hash );
539             }
540             return \%hash;
541             }
542              
543             # overlap for support get by query
544             sub fetch_one {
545             my $self = shift;
546             my ($obj) = values %{ $self->fetch(@_) };
547             $obj;
548             }
549              
550             sub GetLastID {
551             my $self = shift;
552             my $table_name = $self->_table_name();
553             my $field = $self->_key_field;
554             my $res =
555             $self->_query_dbh("select max($field)as res from $table_name")
556             ->fetchrow_hashref;
557             return $res->{res};
558             }
559              
560             1;
561             __END__