File Coverage

blib/lib/Catmandu/Store/DBI/Bag.pm
Criterion Covered Total %
statement 15 70 21.4
branch 0 16 0.0
condition 0 19 0.0
subroutine 5 13 38.4
pod 0 1 0.0
total 20 119 16.8


line stmt bran cond sub pod time code
1             package Catmandu::Store::DBI::Bag;
2              
3 7     7   54 use Catmandu::Sane;
  7         19  
  7         63  
4 7     7   1630 use Moo;
  7         21  
  7         57  
5 7     7   6157 use Catmandu::Store::DBI::Iterator;
  7         24  
  7         223  
6 7     7   55 use namespace::clean;
  7         13  
  7         35  
7              
8             our $VERSION = "0.09";
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 0     0 0   my ($self) = @_;
47 0           $self->_normalize_mapping;
48             }
49              
50             sub _normalize_mapping {
51 0     0     my ($self) = @_;
52 0           my $mapping = $self->mapping;
53              
54 0   0       $mapping->{_id} ||= $default_mapping->{_id};
55              
56 0           for my $key (keys %$mapping) {
57 0           my $map = $mapping->{$key};
58 0   0       $map->{type} ||= 'string';
59 0   0       $map->{column} ||= $key;
60             }
61              
62 0           $mapping;
63             }
64              
65             sub _build_iterator {
66 0     0     my ($self) = @_;
67 0           Catmandu::Store::DBI::Iterator->new(bag => $self);
68             }
69              
70             sub _build_store_with_table {
71 0     0     my ($self) = @_;
72 0           my $store = $self->store;
73 0           $store->handler->create_table($self);
74 0           $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 0     0     my ($self, $row) = @_;
126 0           my $mapping = $self->mapping;
127 0           my $data = {};
128              
129 0           for my $key (keys %$mapping) {
130 0           my $map = $mapping->{$key};
131 0   0       my $val = $row->{$map->{column}} // next;
132 0 0         if ($map->{serialize}) {
133 0           $val = $self->deserialize($val);
134 0 0         if ($map->{serialize} eq 'all') {
135 0           for my $k (keys %$val) {
136 0   0       $data->{$k} = $val->{$k} // next;
137             }
138 0           next;
139             }
140             }
141 0 0         if ($map->{type} eq "datetime") {
142              
143 0           my ($date, $time) = split ' ', $val;
144 0           $val = "${date}T${time}Z";
145              
146             }
147 0           $data->{$key} = $val;
148             }
149              
150 0           $data;
151             }
152              
153             sub _data_to_row {
154 0     0     my ($self, $data) = @_;
155 0           $data = {%$data};
156 0           my $mapping = $self->mapping;
157 0           my $row = {};
158 0           my $serialize_all_column;
159              
160 0           for my $key (keys %$mapping) {
161 0           my $map = $mapping->{$key};
162 0           my $val = delete($data->{$key});
163 0 0         if ($map->{serialize}) {
164 0 0         if ($map->{serialize} eq 'all') {
165 0           $serialize_all_column = $map->{column};
166 0           next;
167             }
168 0   0       $val = $self->serialize($val // next);
169             }
170 0 0         if ($map->{type} eq "datetime") {
171              
172             # Translate ISO dates into datetime format
173 0 0 0       if ($val && $val =~ /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/) {
174 0           $val = "$1 $2";
175             }
176             }
177 0   0       $row->{$map->{column}} = $val // next;
178             }
179              
180 0 0         if ($serialize_all_column) {
181 0           $row->{$serialize_all_column} = $self->serialize($data);
182             }
183              
184 0           $row;
185             }
186              
187             sub _quote_id {
188 0     0     $_[0]->store->dbh->quote_identifier($_[1]);
189             }
190              
191             sub _max_limit { # should be plenty large
192 7     7   16499 use bigint;
  7         24085  
  7         36  
193 0     0     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;