File Coverage

blib/lib/Catmandu/Store/DBI/Bag.pm
Criterion Covered Total %
statement 65 70 92.8
branch 10 16 62.5
condition 8 19 42.1
subroutine 13 13 100.0
pod 0 1 0.0
total 96 119 80.6


line stmt bran cond sub pod time code
1              
2             use Catmandu::Sane;
3 6     6   41 use Moo;
  6         10  
  6         44  
4 6     6   1182 use Catmandu::Store::DBI::Iterator;
  6         12  
  6         40  
5 6     6   4464 use namespace::clean;
  6         16  
  6         171  
6 6     6   38  
  6         10  
  6         27  
7             our $VERSION = "0.12";
8              
9             my $default_mapping = {
10             _id => {
11             column => 'id',
12             type => 'string',
13             index => 1,
14             required => 1,
15             unique => 1,
16             },
17             _data => {column => 'data', type => 'binary', serialize => 'all',}
18             };
19              
20             has mapping => (is => 'ro', default => sub {+{%$default_mapping}},);
21             has default_order => (is => 'ro');
22              
23             has _iterator => (
24             is => 'ro',
25             lazy => 1,
26             builder => '_build_iterator',
27             handles => [
28             qw(
29             generator
30             count
31             slice
32             select
33             detect
34             first
35             )
36             ],
37             );
38              
39             has store_with_table => (is => 'lazy');
40              
41             with 'Catmandu::Bag';
42             with 'Catmandu::Serializer';
43              
44             my ($self) = @_;
45             $self->_normalize_mapping;
46 5     5 0 467 }
47 5         17  
48             my ($self) = @_;
49             my $mapping = $self->mapping;
50              
51 5     5   10 $mapping->{_id} ||= $default_mapping->{_id};
52 5         19  
53             for my $key (keys %$mapping) {
54 5   33     19 my $map = $mapping->{$key};
55             $map->{type} ||= 'string';
56 5         18 $map->{column} ||= $key;
57 12         20 }
58 12   50     29  
59 12   33     29 $mapping;
60             }
61              
62 5         50 my ($self) = @_;
63             Catmandu::Store::DBI::Iterator->new(bag => $self);
64             }
65              
66 4     4   1828 my ($self) = @_;
67 4         60 my $store = $self->store;
68             $store->handler->create_table($self);
69             $store;
70             }
71 5     5   46  
72 5         16 my ($self, $id) = @_;
73 5         73 my $store = $self->store_with_table;
74 5         327 my $dbh = $store->dbh;
75             my $q_name = $dbh->quote_identifier($self->name);
76             my $q_id_field = $dbh->quote_identifier($self->mapping->{_id}->{column});
77             my $sth
78             = $dbh->prepare_cached(
79             "SELECT * FROM ${q_name} WHERE ${q_id_field}=?")
80             or Catmandu::Error->throw($dbh->errstr);
81             $sth->execute($id) or Catmandu::Error->throw($sth->errstr);
82             my $row = $sth->fetchrow_hashref;
83             $sth->finish;
84             $self->_row_to_data($row // return);
85             }
86              
87             my ($self, $data) = @_;
88             $self->store_with_table->handler->add_row($self,
89             $self->_data_to_row($data));
90             $data;
91             }
92              
93             my ($self, $id) = @_;
94             my $store = $self->store_with_table;
95             my $dbh = $store->dbh;
96             my $q_name = $dbh->quote_identifier($self->name);
97             my $q_id_field = $dbh->quote_identifier($self->mapping->{_id}->{column});
98             my $sth
99             = $dbh->prepare_cached("DELETE FROM ${q_name} WHERE ${q_id_field}=?")
100             or Catmandu::Error->throw($dbh->errstr);
101             $sth->execute($id) or Catmandu::Error->throw($sth->errstr);
102             $sth->finish;
103             }
104              
105             my ($self) = @_;
106             my $store = $self->store_with_table;
107             my $dbh = $store->dbh;
108             my $q_name = $dbh->quote_identifier($self->name);
109             my $sth = $dbh->prepare_cached("DELETE FROM ${q_name}")
110             or Catmandu::Error->throw($dbh->errstr);
111             $sth->execute or Catmandu::Error->throw($sth->errstr);
112             $sth->finish;
113             }
114              
115             my ($self, $row) = @_;
116             my $mapping = $self->mapping;
117             my $data = {};
118              
119             for my $key (keys %$mapping) {
120             my $map = $mapping->{$key};
121             my $val = $row->{$map->{column}} // next;
122             if ($map->{serialize}) {
123             $val = $self->deserialize($val);
124             if ($map->{serialize} eq 'all') {
125 2     2   8 for my $k (keys %$val) {
126 2         7 $data->{$k} = $val->{$k} // next;
127 2         15 }
128             next;
129 2         10 }
130 5         8 }
131 5   100     17 if ($map->{type} eq "datetime") {
132 4 100       11  
133 1         24 my ($date, $time) = split ' ', $val;
134 1 50       46 $val = "${date}T${time}Z";
135 1         4  
136 2   50     6 }
137             $data->{$key} = $val;
138 1         4 }
139              
140             $data;
141 3 50       8 }
142              
143 0         0 my ($self, $data) = @_;
144 0         0 $data = {%$data};
145             my $mapping = $self->mapping;
146             my $row = {};
147 3         7 my $serialize_all_column;
148              
149             for my $key (keys %$mapping) {
150 2         9 my $map = $mapping->{$key};
151             my $val = delete($data->{$key});
152             if ($map->{serialize}) {
153             if ($map->{serialize} eq 'all') {
154 50     50   1314 $serialize_all_column = $map->{column};
155 50         318 next;
156 50         182 }
157 50         94 $val = $self->serialize($val // next);
158 50         89 }
159             if ($map->{type} eq "datetime") {
160 50         149  
161 126         193 # Translate ISO dates into datetime format
162 126         191 if ($val && $val =~ /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/) {
163 126 100       294 $val = "$1 $2";
164 24 50       114 }
165 24         89 }
166 24         73 $row->{$map->{column}} = $val // next;
167             }
168 0   0     0  
169             if ($serialize_all_column) {
170 102 50       250 $row->{$serialize_all_column} = $self->serialize($data);
171             }
172              
173 0 0 0     0 $row;
174 0         0 }
175              
176             $_[0]->store->dbh->quote_identifier($_[1]);
177 102   100     308 }
178              
179             use bigint;
180 50 100       121 state $max_limit = 2**63 - 1;
181 24         452 }
182              
183             =head1 NAME
184 50         6170  
185             Catmandu::Store::DBI::Bag - implementation of a Catmandu::Bag for DBI
186              
187             =head1 SYNOPSIS
188 14     14   60  
189             my $store = Catmandu::Store::DBI->new(
190             data_source => "dbi:SQLite:dbname=/tmp/test.db",
191             bags => {
192 6     6   13446 data => {
  6         18673  
  6         25  
193 1     1   56 mapping => {
194             _id => {
195             column => 'id',
196             type => 'string',
197             index => 1,
198             unique => 1
199             },
200             author => {
201             type => 'string'
202             },
203             subject => {
204             type => 'string',
205             },
206             _data => {
207             column => 'data',
208             type => 'binary',
209             serialize => 'all'
210             }
211             }
212             }
213             }
214             );
215              
216             my $bag = $store->bag('data');
217              
218             #SELECT
219             {
220             #SELECT * FROM DATA WHERE author = 'Nicolas'
221             my $iterator = $bag->select( author => 'Nicolas' );
222             }
223             #CHAINED SELECT
224             {
225             #SELECT * FROM DATA WHERE author = 'Nicolas' AND subject = 'ICT'
226             my $iterator = $bag->select( author => 'Nicolas' )->select( subject => 'ICT' );
227             }
228             #COUNT
229             {
230             #SELECT * FROM DATA WHERE author = 'Nicolas'
231             my $iterator = $bag->select( author => 'Nicolas' );
232              
233             #SELECT COUNT(*) FROM ( SELECT * FROM DATA WHERE author = 'Nicolas' )
234             my $count = $iterator->count();
235             }
236             #DETECT
237             {
238             #SELECT * FROM DATA WHERE author = 'Nicolas' AND subject = 'ICT' LIMIT 1
239             my $record = $bag->select( author => 'Nicolas' )->detect( subject => 'ICT' );
240             }
241              
242             #NOTES
243             {
244              
245             #This creates an iterator with a specialized SQL query:
246              
247             #SELECT * FROM DATA WHERE author = 'Nicolas'
248             my $iterator = $bag->select( author => 'Nicolas' );
249              
250             #But this does not
251             my $iterator2 = $iterator->select( title => "Hello world" );
252              
253             #'title' does not have a corresponding table column, so it falls back to the default implementation,
254             #and loops over every record.
255              
256             }
257             {
258              
259             #this is faster..
260             my $iterator = $bag->select( author => 'Nicolas' )->select( title => 'Hello world');
261              
262             #..than
263             my $iterator2 = $bag->select( title => 'Hello world' )->select( author => 'Nicolas' );
264              
265             #reason:
266              
267             # the select statement of $iterator creates a specialized query, and so reduces the amount of records to loop over.
268             # $iterator is a L<Catmandu::Store::DBI::Iterator>.
269              
270             # the select statement of $iterator2 does not have a specialized query, so it's a generic L<Catmandu::Iterator>.
271             # the second select statement of $iterator2 receives this generic object as its source, and can only loop over its records.
272              
273             }
274              
275             =head1 DESCRIPTION
276              
277             Catmandu::Store::DBI::Bag provides some method overrides specific for DBI interfaces,
278             to make querying more efficient.
279              
280             =head1 METHODS
281              
282             =head2 store_with_table
283              
284             Equivalent to the C<store> accessor, but ensures that the table for this bag exists.
285              
286             =head2 select($key => $val)
287              
288             Overrides equivalent method in L<Catmandu::Bag>.
289              
290             Either returns a generic L<Catmandu::Iterator> or a more efficient L<Catmandu::Store::DBI::Iterator>.
291              
292             Expect the following behaviour:
293              
294             =over 4
295              
296             =item
297              
298             the key has a corresponding table column configured
299              
300             a SQL where clause is created in the background:
301              
302             .. WHERE $key = $val
303              
304             Chained select statements with existing table columns result in a combined where clause:
305              
306             .. WHERE $key1 = $val1 AND $key2 = $val2 ..
307              
308             The returned object is a L<Catmandu::Store::DBI::Iterator>, instead of the generic L<Catmandu::Iterator>.
309              
310             =item
311              
312             the key does not have a corresponding table column configured
313              
314             The returned object is a generic L<Catmandu::Iterator>.
315              
316             This iterator can only loop over the records provided by the previous L<Catmandu::Iterable>.
317              
318             =back
319              
320             A few important notes:
321              
322             =over 4
323              
324             =item
325              
326             A select statement only results in a L<Catmandu::Store::DBI::Iterator>, when it has a mapped key,
327             and the previous iterator is either a L<Catmandu::Store::DBI::Bag> or a L<Catmandu::Store::DBI::Iterator>.
328              
329             =item
330              
331             As soon as the returned object is a generic L<Catmandu::Iterator>, any following select statement
332             with mapped columns will not make a more efficient L<Catmandu::Store::DBI::Iterator>.
333              
334             =back
335              
336             In order to make your chained statements efficient, do the following:
337              
338             =over 4
339              
340             =item
341              
342             create indexes on the table columns
343              
344             =item
345              
346             put select statements with mapped keys in front, and those with non mapped keys at the end.
347              
348             =back
349              
350             To configure table columns, see L<Catmandu::Store::DBI>.
351              
352             =head2 detect($key => $val)
353              
354             Overrides equivalent method in L<Catmandu::Bag>.
355              
356             Also returns first record where $key matches $val.
357              
358             Works like the select method above, but adds the SQL statement 'LIMIT 1' to the current SQL query in the background.
359              
360             =head2 first()
361              
362             Overrides equivalent method in L<Catmandu::Bag>.
363              
364             Also returns first record using the current iterator.
365              
366             The parent method uses a generator, but fetches only one record.
367              
368             This method adds the SQL statement 'LIMIT 1' to the current SQL query.
369              
370             =head2 count()
371              
372             Overrides equivalent method in L<Catmandu::Bag>.
373              
374             When the source is a L<Catmandu::Store::DBI::Bag>, or a L<Catmandu::Store::DBI::Iterator>,
375             a specialized SQL query is created:
376              
377             SELECT COUNT(*) FROM TABLE WHERE (..)
378              
379             The select statement of the source is between the parenthesises.
380              
381             =cut
382              
383             1;