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