File Coverage

blib/lib/BusyBird/StatusStorage/SQLite.pm
Criterion Covered Total %
statement 456 485 94.0
branch 169 202 83.6
condition 66 81 81.4
subroutine 59 67 88.0
pod 9 9 100.0
total 759 844 89.9


line stmt bran cond sub pod time code
1             package BusyBird::StatusStorage::SQLite;
2 12     12   53773 use v5.8.0;
  12         31  
  12         513  
3 12     12   62 use strict;
  12         18  
  12         412  
4 12     12   58 use warnings;
  12         21  
  12         472  
5 12     12   61 use parent ("BusyBird::StatusStorage");
  12         23  
  12         108  
6 12     12   17709 use DBI;
  12         198533  
  12         958  
7 12     12   131 use Carp;
  12         21  
  12         916  
8 12     12   67 use Try::Tiny;
  12         20  
  12         739  
9 12     12   8578 use SQL::Maker 1.19;
  12         165113  
  12         650  
10 12     12   8237 use SQL::QueryMaker 0.03 qw(sql_and sql_eq sql_ne sql_or sql_lt sql_le sql_raw);
  12         34892  
  12         1511  
11 12     12   119 use BusyBird::DateTime::Format;
  12         19  
  12         384  
12 12     12   64 use BusyBird::Util qw(set_param);
  12         23  
  12         654  
13 12     12   8611 use JSON;
  12         121650  
  12         94  
14 12     12   2175 use Scalar::Util qw(looks_like_number);
  12         25  
  12         926  
15 12     12   74 use DateTime::Format::Strptime;
  12         21  
  12         534  
16 12     12   74 use DateTime;
  12         21  
  12         298  
17 12     12   4769 no autovivification;
  12         6845  
  12         93  
18              
19             my @STATUSES_ORDER_BY = ('utc_acked_at DESC', 'utc_created_at DESC', 'status_id DESC');
20             my $DELETE_COUNT_ID = 0;
21              
22             my $UNDEF_TIMESTAMP = '9999-99-99T99:99:99';
23              
24             {
25             my $TIMESTAMP_FORMAT_STR = '%Y-%m-%dT%H:%M:%S';
26             my $TIMESTAMP_FORMAT = DateTime::Format::Strptime->new(
27             pattern => $TIMESTAMP_FORMAT_STR,
28             time_zone => 'UTC',
29             on_error => 'croak',
30             );
31              
32             sub _format_datetime {
33 4539     4539   48172 my ($dt) = @_;
34 4539         11789 return $dt->strftime($TIMESTAMP_FORMAT_STR);
35             }
36              
37             sub _parse_datetime {
38 15621     15621   15737 my ($dt_str) = @_;
39 15621         50129 return $TIMESTAMP_FORMAT->parse_datetime($dt_str);
40             }
41             }
42              
43              
44             sub new {
45 183     183 1 225997 my ($class, %args) = @_;
46 183         1509 my $self = bless {
47             maker => SQL::Maker->new(driver => 'SQLite', strict => 1),
48             in_memory_dbh => undef,
49             }, $class;
50 183         6872 $self->set_param(\%args, "path", undef, 1);
51 183         621 $self->set_param(\%args, "max_status_num", 2000);
52 183         1331 $self->set_param(\%args, "hard_max_status_num", int($self->{max_status_num} * 1.2));
53 183         849 $self->set_param(\%args, "vacuum_on_delete", int($self->{max_status_num} * 2.0));
54 183 50       1178 croak "max_status_num must be a number" if !looks_like_number($self->{max_status_num});
55 183 50       885 croak "hard_max_status_num must be a number" if !looks_like_number($self->{hard_max_status_num});
56 183         691 $self->{max_status_num} = int($self->{max_status_num});
57 183         530 $self->{hard_max_status_num} = int($self->{hard_max_status_num});
58 183 50       862 croak "hard_max_status_num must be >= max_status_num" if !($self->{hard_max_status_num} >= $self->{max_status_num});
59 183         731 $self->_create_tables();
60 182         150462 return $self;
61             }
62              
63             sub _create_new_dbh {
64 821     821   2583 my ($self, @connect_params) = @_;
65 821         6318 my $dbh = DBI->connect(@connect_params);
66 820         483841 $dbh->do(q{PRAGMA foreign_keys = ON});
67 820         93602 return $dbh;
68             }
69              
70             sub _get_my_dbh {
71 2649     2649   6213 my ($self) = @_;
72 2649         24311 my @connect_params = ("dbi:SQLite:dbname=$self->{path}", "", "", {
73             RaiseError => 1, PrintError => 0, AutoCommit => 1, sqlite_unicode => 1,
74             });
75 2649 100       11837 if($self->{path} eq ':memory:') {
76 2000 100       8781 $self->{in_memory_dbh} = $self->_create_new_dbh(@connect_params) if !$self->{in_memory_dbh};
77 2000         8991 return $self->{in_memory_dbh};
78             }
79 649         2974 return $self->_create_new_dbh(@connect_params);
80             }
81              
82             sub _create_tables {
83 183     183   327 my ($self) = @_;
84 183         608 my $dbh = $self->_get_my_dbh();
85 182         899 $dbh->do(<
86             CREATE TABLE IF NOT EXISTS timelines (
87             timeline_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
88             name TEXT UNIQUE NOT NULL
89             )
90             EOD
91 182         332455 $dbh->do(<
92             CREATE TABLE IF NOT EXISTS statuses (
93             timeline_id INTEGER NOT NULL
94             REFERENCES timelines(timeline_id) ON DELETE CASCADE ON UPDATE CASCADE,
95             status_id TEXT NOT NULL,
96             utc_acked_at TEXT NOT NULL,
97             utc_created_at TEXT NOT NULL,
98             timezone_acked_at TEXT NOT NULL,
99             timezone_created_at TEXT NOT NULL,
100             level INTEGER NOT NULL,
101             content TEXT NOT NULL,
102              
103             PRIMARY KEY (timeline_id, status_id)
104             )
105             EOD
106 182         401860 $dbh->do(<
107             CREATE TABLE IF NOT EXISTS delete_counts (
108             delete_count_id INTEGER PRIMARY KEY NOT NULL,
109             delete_count INTEGER NOT NULL
110             )
111             EOD
112 182         251524 my ($sql, @bind) = $self->{maker}->insert('delete_counts', [
113             delete_count_id => $DELETE_COUNT_ID, delete_count => 0
114             ], {prefix => 'INSERT OR IGNORE INTO'});
115 182         23123 $dbh->do($sql, undef, @bind);
116             }
117              
118             sub _record_hash_to_array {
119 3283     3283   3808 my ($record) = @_;
120 3283         19217 return [ map { $_, $record->{$_} } sort { $a cmp $b } keys %$record ];
  26264         56033  
  50888         44332  
121             }
122              
123             sub _put_update {
124 426     426   898 my ($self, $dbh, $record, $prev_sth) = @_;
125 426         601 my $sth = $prev_sth;
126 426         1258 my ($sql, @bind) = $self->{maker}->update('statuses', _record_hash_to_array($record), sql_and([
127             sql_eq('timeline_id' => $record->{timeline_id}), sql_eq(status_id => $record->{status_id})
128             ]));
129 426 100       161193 if(!$sth) {
130             ## Or, should we check $sql is not changed...?
131 111         1043 $sth = $dbh->prepare($sql);
132             }
133 426         48152 return ($sth->execute(@bind), $sth);
134             }
135              
136             sub _put_insert {
137 2857     2857   4709 my ($self, $dbh, $record, $prev_sth) = @_;
138 2857         3763 my $sth = $prev_sth;
139 2857         7836 my ($sql, @bind) = $self->{maker}->insert('statuses', _record_hash_to_array($record), {
140             prefix => 'INSERT OR IGNORE INTO'
141             });
142 2857 100       599110 if(!$sth) {
143 371         3204 $sth = $dbh->prepare($sql);
144             }
145 2857         148120 return ($sth->execute(@bind), $sth);
146             }
147              
148             sub _put_upsert {
149 68     68   121 my ($self, $dbh, $record) = @_;
150 68         177 my ($count) = $self->_put_update($dbh, $record);
151 68 100       1299 if($count <= 0) {
152 34         117 ($count) = $self->_put_insert($dbh, $record);
153             }
154 68         610 return ($count, undef);
155             }
156              
157             sub put_statuses {
158 430     430 1 240455 my ($self, %args) = @_;
159 430         1072 my $timeline = $args{timeline};
160 430 100       1403 croak "timeline parameter is mandatory" if not defined $timeline;
161 428         794 my $mode = $args{mode};
162 428 100       1559 croak "mode parameter is mandatory" if not defined $mode;
163 426 50 100     2222 if($mode ne 'insert' && $mode ne 'update' && $mode ne 'upsert') {
      66        
164 0         0 croak "mode must be either insert, update or upsert";
165             }
166 426         989 my $statuses = $args{statuses};
167 426 100       1361 croak "statuses parameter is mandatory" if not defined $statuses;
168 424 50 66     2788 if(ref($statuses) ne 'HASH' && ref($statuses) ne 'ARRAY') {
169 0         0 croak "statuses parameter must be either a status object or an array-ref of statuses";
170             }
171 424 100       1294 if(ref($statuses) eq 'HASH') {
172 82         191 $statuses = [$statuses];
173             }
174 424         1214 foreach my $status (@$statuses) {
175 3309 100 66     17217 croak "status object must be a hash-ref" if !defined($status) || !ref($status) || ref($status) ne 'HASH';
      66        
176 3303 100       10157 croak "status ID is missing" if not defined $status->{id};
177             }
178 394   50 0   1527 my $callback = $args{callback} || sub {};
  0         0  
179 394         549 my $dbh;
180             my @results = try {
181 394 100   394   15119 return (undef, 0) if @$statuses == 0;
182 388         1526 $dbh = $self->_get_my_dbh();
183 388         3640 $dbh->begin_work();
184 388   66     8755 my $timeline_id = $self->_get_timeline_id($dbh, $timeline) || $self->_create_timeline($dbh, $timeline);
185 388 50       1362 if(!defined($timeline_id)) {
186 0         0 die "Internal error: could not create a timeline '$timeline' somehow.";
187             }
188 388         569 my $sth;
189 388         749 my $total_count = 0;
190 388         1316 my $put_method = "_put_$mode";
191 388         1098 foreach my $status (@$statuses) {
192 3267         8115 my $record = _to_status_record($timeline_id, $status);
193 3249         4583 my $count;
194 3249         10440 ($count, $sth) = $self->$put_method($dbh, $record, $sth);
195 3249 100       10004 if($count > 0) {
196 3211         13297 $total_count += $count;
197             }
198             }
199 370         774 my $exceeding_delete_count = 0;
200 370 100 100     2735 if($mode ne "update" && $total_count > 0) {
201 325         1509 $exceeding_delete_count = $self->_delete_exceeding_statuses($dbh, $timeline_id);
202             }
203 370         3621865 $dbh->commit();
204 370 100       1870 if($exceeding_delete_count > 0) {
205 8         66 $self->_add_to_delete_count($dbh, $exceeding_delete_count);
206             }
207 370         164840 return (undef, $total_count);
208             } catch {
209 18     18   969 my $e = shift;
210 18 50       76 if($dbh) {
211 18         2077 $dbh->rollback();
212             }
213 18         117 return ($e);
214 394         4828 };
215 394         13935 @_ = @results;
216 394         16706 goto $callback;
217             }
218              
219             sub _get_timeline_id {
220 2721     2721   5535 my ($self, $dbh, $timeline_name) = @_;
221 2721         18986 my ($sql, @bind) = $self->{maker}->select('timelines', ['timeline_id'], sql_eq(name => $timeline_name));
222 2721         945171 my $record = $dbh->selectrow_arrayref($sql, undef, @bind);
223 2721 100       502673 if(!defined($record)) {
224 644         3787 return undef;
225             }
226 2077         12630 return $record->[0];
227             }
228              
229             sub _create_timeline {
230 267     267   680 my ($self, $dbh, $timeline_name) = @_;
231 267         2273 my ($sql, @bind) = $self->{maker}->insert('timelines', [name => "$timeline_name"]);
232 267         24837 $dbh->do($sql, undef, @bind);
233 267         50478 return $self->_get_timeline_id($dbh, $timeline_name);
234             }
235              
236             sub _to_status_record {
237 3267     3267   5437 my ($timeline_id, $status) = @_;
238 3267 50       11210 croak "status ID must be set" if not defined $status->{id};
239 3267 50       6124 croak "timeline_id must be defined" if not defined $timeline_id;
240 3267   100     27961 my $record = {
241             timeline_id => $timeline_id,
242             status_id => $status->{id},
243             level => $status->{busybird}{level} || 0,
244             };
245 3261         9249 my $acked_at = $status->{busybird}{acked_at}; ## avoid autovivification
246 3261         8299 ($record->{utc_acked_at}, $record->{timezone_acked_at}) = _extract_utc_timestamp_and_timezone($acked_at);
247 3255         7685 ($record->{utc_created_at}, $record->{timezone_created_at}) = _extract_utc_timestamp_and_timezone($status->{created_at});
248 3249         12062 $record->{content} = to_json($status);
249 3249         68184 return $record;
250             }
251              
252             sub _from_status_record {
253 10319     10319   14643 my ($record) = @_;
254 10319         40236 my $status = from_json($record->{content});
255 10319         202783 $status->{id} = $record->{status_id};
256 10319 100 100     75832 if($record->{level} != 0 || defined($status->{busybird}{level})) {
257 2760         6595 $status->{busybird}{level} = $record->{level};
258             }
259 10319         27152 my $acked_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_acked_at}, $record->{timezone_acked_at});
260 10319 100 66     1276766 if(defined($acked_at_str) || defined($status->{busybird}{acked_at})) {
261 5328         15007 $status->{busybird}{acked_at} = $acked_at_str;
262             }
263 10319         24158 my $created_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_created_at}, $record->{timezone_created_at});
264 10319 100 66     2408948 if(defined($created_at_str) || defined($status->{created_at})) {
265 10293         23803 $status->{created_at} = $created_at_str;
266             }
267 10319         369876 return $status;
268             }
269              
270             sub _extract_utc_timestamp_and_timezone {
271 6516     6516   7273 my ($timestamp_str) = @_;
272 6516 100 66     24270 if(!defined($timestamp_str) || $timestamp_str eq '') {
273 2106         7485 return ($UNDEF_TIMESTAMP, 'UTC');
274             }
275 4410         15901 my $datetime = BusyBird::DateTime::Format->parse_datetime($timestamp_str);
276 4410 100       4299417 croak "Invalid datetime format: $timestamp_str" if not defined $datetime;
277 4398         13047 my $timezone_name = $datetime->time_zone->name;
278 4398         38732 $datetime->set_time_zone('UTC');
279 4398         38053 my $utc_timestamp = _format_datetime($datetime);
280 4398         323120 return ($utc_timestamp, $timezone_name);
281             }
282              
283             sub _create_bb_timestamp_from_utc_timestamp_and_timezone {
284 20638     20638   27909 my ($utc_timestamp_str, $timezone) = @_;
285 20638 100       44504 if($utc_timestamp_str eq $UNDEF_TIMESTAMP) {
286 5017         9748 return undef;
287             }
288 15621         27854 my $dt = _parse_datetime($utc_timestamp_str);
289 15621         9538353 $dt->set_time_zone($timezone);
290 15621         177097 return BusyBird::DateTime::Format->format_datetime($dt);
291             }
292              
293             sub get_statuses {
294 1142     1142 1 143431 my ($self, %args) = @_;
295 1142         2951 my $timeline = $args{timeline};
296 1142 100       4480 croak "timeline parameter is mandatory" if not defined $timeline;
297 1140         2153 my $callback = $args{callback};
298 1140 100       3981 croak "callback parameter is mandatory" if not defined $callback;
299 1138 50       5016 croak "callback parameter must be a CODEREF" if ref($callback) ne "CODE";
300 1138 100       4578 my $ack_state = defined($args{ack_state}) ? $args{ack_state} : "any";
301 1138 50 100     8030 if($ack_state ne "any" && $ack_state ne "unacked" && $ack_state ne "acked") {
      66        
302 0         0 croak "ack_state parameter must be either 'any' or 'acked' or 'unacked'";
303             }
304 1138         3034 my $max_id = $args{max_id};
305 1138 100       4051 my $count = defined($args{count}) ? $args{count} : 'all';
306 1138 50 66     5956 if($count ne 'all' && !looks_like_number($count)) {
307 0         0 croak "count parameter must be either 'all' or number";
308             }
309             my @results = try {
310 1138     1138   53145 my $dbh = $self->_get_my_dbh();
311 1138         5092 my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
312 1138 100       4618 if(!defined($timeline_id)) {
313 86         2768 return (undef, []);
314             }
315 1052         4539 my $cond = $self->_create_base_condition($timeline_id, $ack_state);
316 1052 100       3790 if(defined($max_id)) {
317 193         798 my $max_id_cond = $self->_create_max_id_condition($dbh, $timeline_id, $max_id, $ack_state);
318 193 100       742 if(!defined($max_id_cond)) {
319 78         4080 return (undef, []);
320             }
321 115         432 $cond = sql_and([$cond, $max_id_cond]);
322             }
323 974         7526 my %maker_opt = (order_by => \@STATUSES_ORDER_BY);
324 974 100       3523 if($count ne 'all') {
325 204         684 $maker_opt{limit} = "$count";
326             }
327 974         5391 my ($sql, @bind) = $self->{maker}->select("statuses", ['*'], $cond, \%maker_opt);
328 974         323528 my $sth = $dbh->prepare($sql);
329 974         191069 $sth->execute(@bind);
330 974         3406 my @statuses = ();
331 974         48144 while(my $record = $sth->fetchrow_hashref('NAME_lc')) {
332 10319         25180 push(@statuses, _from_status_record($record));
333             }
334 974         107714 return (undef, \@statuses);
335             }catch {
336 0     0   0 my $e = shift;
337 0         0 return ($e);
338 1138         14464 };
339 1138         35366 @_ = @results;
340 1138         9155 goto $callback;
341             }
342              
343             sub _create_base_condition {
344 1856     1856   4051 my ($self, $timeline_id, $ack_state) = @_;
345 1856   50     5410 $ack_state ||= 'any';
346 1856         6558 my $cond = sql_eq(timeline_id => $timeline_id);
347 1856 100       46947 if($ack_state eq 'acked') {
    100          
348 369         1567 $cond = sql_and([$cond, sql_ne(utc_acked_at => $UNDEF_TIMESTAMP)]);
349             }elsif($ack_state eq 'unacked') {
350 970         2764 $cond = sql_and([$cond, sql_eq(utc_acked_at => $UNDEF_TIMESTAMP)]);
351             }
352 1856         74025 return $cond;
353             }
354              
355             sub _get_timestamps_of {
356 244     244   569 my ($self, $dbh, $timeline_id, $status_id, $ack_state) = @_;
357 244         804 my $cond = $self->_create_base_condition($timeline_id, $ack_state);
358 244         885 $cond = sql_and([$cond, sql_eq(status_id => $status_id)]);
359 244         13959 my ($sql, @bind) = $self->{maker}->select("statuses", ['utc_acked_at', 'utc_created_at'], $cond, {
360             limit => 1
361             });
362 244         105014 my $record = $dbh->selectrow_arrayref($sql, undef, @bind);
363 244 100       34419 if(!$record) {
364 96         2460 return ();
365             }
366 148         3729 return ($record->[0], $record->[1]);
367             }
368              
369             sub _create_max_id_condition {
370 244     244   636 my ($self, $dbh, $timeline_id, $max_id, $ack_state) = @_;
371 244         1050 my ($max_acked_at, $max_created_at) = $self->_get_timestamps_of($dbh, $timeline_id, $max_id, $ack_state);
372 244 100 66     1422 if(!defined($max_acked_at) || !defined($max_created_at)) {
373 96         238 return undef;
374             }
375 148         637 return $self->_create_max_time_condition($max_acked_at, $max_created_at, $max_id);
376             }
377              
378             sub _create_max_time_condition {
379 156     156   422 my ($self, $max_acked_at, $max_created_at, $max_id) = @_;
380 156         847 my $cond = sql_or([
381             sql_lt(utc_acked_at => $max_acked_at),
382             sql_and([
383             sql_eq(utc_acked_at => $max_acked_at),
384             sql_or([
385             sql_lt(utc_created_at => $max_created_at),
386             sql_and([
387             sql_eq(utc_created_at => $max_created_at),
388             sql_le(status_id => $max_id),
389             ])
390             ])
391             ])
392             ]);
393 156         31123 return $cond;
394             }
395              
396             sub ack_statuses {
397 145     145 1 5278 my ($self, %args) = @_;
398 145         403 my $timeline = $args{timeline};
399 145 100       876 croak "timeline parameter is mandatory" if not defined $timeline;
400 143 50   0   552 my $callback = defined($args{callback}) ? $args{callback} : sub {};
  0         0  
401 143 50       596 croak "callback parameter must be a CODEREF" if ref($callback) ne 'CODE';
402 143         334 my $ids = $args{ids};
403 143 50 100     1100 if(defined($ids) && ref($ids) && ref($ids) ne 'ARRAY') {
      66        
404 0         0 croak "ids parameter must be either undef, a status ID or an array-ref of status IDs";
405             }
406 143 100 100     789 if(defined($ids) && !ref($ids)) {
407 19         57 $ids = [$ids];
408             }
409 143 100 100     691 if(defined($ids) && grep { !defined($_) } @$ids) {
  190         473  
410 2         253 croak "ids parameter array must not contain undef.";
411             }
412 141         511 my $max_id = $args{max_id};
413 141         239 my $dbh;
414             my @results = try {
415 141     141   6219 my $ack_utc_timestamp = _format_datetime(DateTime->now(time_zone => 'UTC'));
416 141         13224 $dbh = $self->_get_my_dbh();
417 141         597 my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
418 141 50       528 return (undef, 0) if not defined $timeline_id;
419 141         992 $dbh->begin_work();
420 141         2787 my $total_count = 0;
421 141 100 100     918 if(!defined($ids) && !defined($max_id)) {
422 36         157 $total_count = $self->_ack_all($dbh, $timeline_id, $ack_utc_timestamp);
423             }else {
424 105 100       482 if(defined($max_id)) {
425 51         263 my $max_id_count = $self->_ack_max_id($dbh, $timeline_id, $ack_utc_timestamp, $max_id);
426 51 100       11277 $total_count += $max_id_count if $max_id_count > 0;
427             }
428 105 100       439 if(defined($ids)) {
429 71         360 my $ids_count = $self->_ack_ids($dbh, $timeline_id, $ack_utc_timestamp, $ids);
430 71 100       301 $total_count += $ids_count if $ids_count > 0;
431             }
432             }
433 141         648458 $dbh->commit();
434 141 50       832 $total_count = 0 if $total_count < 0;
435 141         1421 return (undef, $total_count);
436             }catch {
437 0     0   0 my $e = shift;
438 0 0       0 if($dbh) {
439 0         0 $dbh->rollback();
440             }
441 0         0 return ($e);
442 141         1685 };
443 141         4398 @_ = @results;
444 141         5078 goto $callback;
445             }
446              
447             sub _ack_all {
448 36     36   99 my ($self, $dbh, $timeline_id, $ack_utc_timestamp) = @_;
449 36         237 my ($sql, @bind) = $self->{maker}->update(
450             'statuses', [utc_acked_at => $ack_utc_timestamp],
451             sql_and([sql_eq(timeline_id => $timeline_id), sql_eq(utc_acked_at => $UNDEF_TIMESTAMP)]),
452             );
453 36         8938 return $dbh->do($sql, undef, @bind);
454             }
455              
456             sub _ack_max_id {
457 51     51   192 my ($self, $dbh, $timeline_id, $ack_utc_timestamp, $max_id) = @_;
458 51         236 my $max_id_cond = $self->_create_max_id_condition($dbh, $timeline_id, $max_id, 'unacked');
459 51 100       188 if(!defined($max_id_cond)) {
460 18         95 return 0;
461             }
462 33         107 my $cond = $self->_create_base_condition($timeline_id, 'unacked');
463 33         201 my ($sql, @bind) = $self->{maker}->update(
464             'statuses', [utc_acked_at => $ack_utc_timestamp], sql_and([$cond, $max_id_cond])
465             );
466 33         11820 return $dbh->do($sql, undef, @bind);
467             }
468              
469             sub _ack_ids {
470 71     71   200 my ($self, $dbh, $timeline_id, $ack_utc_timestamp, $ids) = @_;
471 71 100       255 if(@$ids == 0) {
472 14         42 return 0;
473             }
474 57         102 my $total_count = 0;
475 57         108 my $sth;
476 57         178 foreach my $id (@$ids) {
477 184         540 my $cond = $self->_create_base_condition($timeline_id, 'unacked');
478 184         447 $cond = sql_and([$cond, sql_eq(status_id => $id)]);
479 184         7988 my ($sql, @bind) = $self->{maker}->update(
480             'statuses', [utc_acked_at => $ack_utc_timestamp], $cond
481             );
482 184 100       27926 if(!$sth) {
483 57         397 $sth = $dbh->prepare($sql);
484             }
485 184         11870 my $count = $sth->execute(@bind);
486 184 100       2516 if($count > 0) {
487 94         2157 $total_count += $count;
488             }
489             }
490 57         797 return $total_count;
491             }
492              
493             sub delete_statuses {
494 151     151 1 74803 my ($self, %args) = @_;
495 151         420 my $timeline = $args{timeline};
496 151 100       803 croak 'timeline parameter is mandatory' if not defined $timeline;
497 149 100       1025 croak 'ids parameter is mandatory' if not exists $args{ids};
498 147         329 my $ids = $args{ids};
499 147 50 100     870 if(defined($ids) && ref($ids) && ref($ids) ne 'ARRAY') {
      66        
500 0         0 croak 'ids parameter must be either undef, a status ID or array-ref of status IDs.';
501             }
502 147 100 100     635 if(defined($ids) && !ref($ids)) {
503 15         47 $ids = [$ids];
504             }
505 147 100 100     565 if(defined($ids) && grep { !defined($_) } @$ids) {
  90         289  
506 2         193 croak "ids parameter array must not contain undef.";
507             }
508 145 50   0   594 my $callback = defined($args{callback}) ? $args{callback} : sub {};
  0         0  
509 145 50       588 croak 'callback parameter must be a CODEREF' if ref($callback) ne 'CODE';
510 145         169 my $dbh;
511             my @results = try {
512 145     145   6982 my $dbh = $self->_get_my_dbh();
513 145         661 my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
514 145 100       906 if(!defined($timeline_id)) {
515 27         1161 return (undef, 0);
516             }
517 118         926 $dbh->begin_work();
518 118         2289 my $total_count;
519 118 100       500 if(defined($ids)) {
520 31         174 $total_count = $self->_delete_ids($dbh, $timeline_id, $ids);
521             }else {
522 87         452 $total_count = $self->_delete_timeline($dbh, $timeline_id);
523             }
524 118 50       527 $total_count = 0 if $total_count < 0;
525 118         1633836 $dbh->commit();
526 118         1103 $self->_add_to_delete_count($dbh, $total_count);
527 118         341208 return (undef, $total_count);
528             }catch {
529 0     0   0 my $e = shift;
530 0 0       0 if($dbh) {
531 0         0 $dbh->rollback();
532             }
533 0         0 return ($e);
534 145         1745 };
535 145         4144 @_ = @results;
536 145         1135 goto $callback;
537             }
538              
539             sub _delete_timeline {
540 87     87   216 my ($self, $dbh, $timeline_id) = @_;
541 87         677 my ($sql, @bind) = $self->{maker}->delete('statuses', sql_eq(timeline_id => $timeline_id));
542 87         9845 my $status_count = $dbh->do($sql, undef, @bind);
543 87         27733 ($sql, @bind) = $self->{maker}->delete('timelines', sql_eq(timeline_id => $timeline_id));
544 87         8873 $dbh->do($sql, undef, @bind);
545 87         14851 return $status_count;
546             }
547              
548             sub _delete_ids {
549 31     31   70 my ($self, $dbh, $timeline_id, $ids) = @_;
550 31 50       123 return 0 if @$ids == 0;
551 31         71 my $sth;
552 31         54 my $total_count = 0;
553 31         100 foreach my $id (@$ids) {
554 84         338 my ($sql, @bind) = $self->{maker}->delete('statuses', sql_and([
555             sql_eq(timeline_id => $timeline_id), sql_eq(status_id => $id)
556             ]));
557 84 100       15311 if(!$sth) {
558 31         233 $sth = $dbh->prepare($sql);
559             }
560 84         7096 my $count = $sth->execute(@bind);
561 84 100       390 if($count > 0) {
562 70         210 $total_count += $count;
563             }
564             }
565 31         525 return $total_count;
566             }
567              
568             sub _delete_exceeding_statuses {
569 325     325   639 my ($self, $dbh, $timeline_id) = @_;
570             ## get total count in the timeline
571 325         2217 my ($sql, @bind) = $self->{maker}->select('statuses', [\'count(*)'], sql_eq(timeline_id => $timeline_id));
572 325         110001 my $row = $dbh->selectrow_arrayref($sql, undef, @bind);
573 325 50       46463 if(!defined($row)) {
574 0         0 die "count query for timeline $timeline_id returns undef. something is wrong.";
575             }
576 325         1167 my $total_count = $row->[0];
577            
578 325 100       1637 if($total_count <= $self->{hard_max_status_num}) {
579 317         1303 return 0;
580             }
581              
582             ## get the top of the exceeding statuses
583 8         62 ($sql, @bind) = $self->{maker}->select('statuses', [qw(utc_acked_at utc_created_at status_id)], sql_eq(timeline_id => $timeline_id), {
584             order_by => \@STATUSES_ORDER_BY,
585             offset => $self->{max_status_num},
586             limit => 1,
587             });
588 8         3477 $row = $dbh->selectrow_arrayref($sql, undef, @bind);
589 8 50       1716 if(!defined($row)) {
590 0         0 die "selecting the top of exceeding status returns undef. something is wrong.";
591             }
592 8         92 my $time_cond = $self->_create_max_time_condition(@$row);
593              
594             ## execute deletion
595 8         30 my $timeline_cond = sql_eq(timeline_id => $timeline_id);
596 8         154 ($sql, @bind) = $self->{maker}->delete('statuses', sql_and([$timeline_cond, $time_cond]));
597 8         2328 return $dbh->do($sql, undef, @bind);
598             }
599              
600             sub get_unacked_counts {
601 607     607 1 217687 my ($self, %args) = @_;
602 607         1603 my $timeline = $args{timeline};
603 607 100       2030 croak 'timeline parameter is mandatory' if not defined $timeline;
604 605         1205 my $callback = $args{callback};
605 605 100       1893 croak 'callback parameter is mandatory' if not defined $callback;
606 603 50       2350 croak 'callback parameter must be a CODEREF' if ref($callback) ne 'CODE';
607             my @results = try {
608 603     603   25066 my $dbh = $self->_get_my_dbh();
609 603         2189 my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
610 603         2452 my %result_obj = (total => 0);
611 603 100       1685 if(!defined($timeline_id)) {
612 260         1323 return (undef, \%result_obj);
613             }
614 343         1535 my $cond = $self->_create_base_condition($timeline_id, 'unacked');
615 343         2253 my ($sql, @bind) = $self->{maker}->select('statuses', ['level', \'count(status_id)'], $cond, {
616             group_by => 'level'
617             });
618 343         108686 my $sth = $dbh->prepare($sql);
619 343         43579 $sth->execute(@bind);
620 343         3937 while(my $record = $sth->fetchrow_arrayref()) {
621 395         1243 $result_obj{total} += $record->[1];
622 395         3153 $result_obj{$record->[0]} = $record->[1];
623             }
624 343         10904 return (undef, \%result_obj);
625             }catch {
626 0     0   0 my $e = shift;
627 0         0 return ($e);
628 603         6911 };
629 603         13707 @_ = @results;
630 603         3175 goto $callback;
631             }
632              
633             sub _add_to_delete_count {
634 126     126   417 my ($self, $dbh, $add_count) = @_;
635 126 50       1202 return if $self->{vacuum_on_delete} <= 0;
636 126 100       431 return if $add_count <= 0;
637 122         1308 my ($sql, @bind) = $self->{maker}->update(
638             'delete_counts',
639             [delete_count => sql_raw('delete_count + ?', $add_count)],
640             sql_eq(delete_count_id => $DELETE_COUNT_ID));
641 122         31782 $dbh->do($sql, undef, @bind);
642            
643 122         1954203 ($sql, @bind) = $self->{maker}->select('delete_counts', ["delete_count"], sql_eq(delete_count_id => $DELETE_COUNT_ID));
644 122         52660 my $row = $dbh->selectrow_arrayref($sql, undef, @bind);
645 122 50       21150 if(!defined($row)) {
646 0         0 die "no delete_counts row with delete_count_id = $DELETE_COUNT_ID. something is wrong.";
647             }
648 122         589 my $current_delete_count = $row->[0];
649              
650 122 100       887 if($current_delete_count >= $self->{vacuum_on_delete}) {
651 17         126 $self->_do_vacuum($dbh);
652             }
653             }
654              
655             sub _do_vacuum {
656 19     19   2500 my ($self, $dbh) = @_;
657 19         132 my ($sql, @bind) = $self->{maker}->update('delete_counts', [delete_count => 0], sql_eq(delete_count_id => $DELETE_COUNT_ID));
658 19         3086 $dbh->do($sql, undef, @bind);
659 19         375260 $dbh->do('VACUUM');
660             }
661              
662             sub vacuum {
663 2     2 1 4091 my ($self) = @_;
664 2         14 $self->_do_vacuum($self->_get_my_dbh());
665             }
666              
667             sub contains {
668 47     47 1 6608 my ($self, %args) = @_;
669 47         113 my $timeline = $args{timeline};
670 47 100       405 croak 'timeline parameter is mandatory' if not defined $timeline;
671 45         100 my $callback = $args{callback};
672 45 100       355 croak 'callback parameter is mandatory' if not defined $callback;
673 43         80 my $query = $args{query};
674 43 100       361 croak 'query parameter is mandatory' if not defined $query;
675 41         95 my $ref_query = ref($query);
676 41 100 100     370 if(!$ref_query || $ref_query eq 'HASH') {
    50          
677 16         41 $query = [$query];
678             }elsif($ref_query eq 'ARRAY') {
679             ;
680             }else {
681 0         0 croak 'query parameter must be either a status, status ID or array-ref';
682             }
683 41 100       127 if(grep { !defined($_) } @$query) {
  195         392  
684 2         194 croak "query element must be defined";
685             }
686             my @method_result = try {
687 39     39   1668 my $dbh = $self->_get_my_dbh();
688 39         198 my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
689 39         94 my @ret_contained = ();
690 39         97 my @ret_not_contained = ();
691 39 100       145 if(!defined($timeline_id)) {
692 4         21 @ret_not_contained = @$query;
693 4         136 return (undef, \@ret_contained, \@ret_not_contained);
694             }
695 35         53 my $sth;
696 35         104 foreach my $query_elem (@$query) {
697 132 100       427 my $status_id = (ref($query_elem) eq 'HASH') ? $query_elem->{id} : $query_elem;
698 132 100       280 if(!defined($status_id)) {
699             ## ID-less statuses are always 'not contained'.
700 12         28 push @ret_not_contained, $query_elem;
701 12         34 next;
702             }
703 120         639 my ($sql, @bind) = $self->{maker}->select(
704             'statuses', ['timeline_id', 'status_id'],
705             sql_and([sql_eq(timeline_id => $timeline_id), sql_eq(status_id => $status_id)])
706             );
707 120 100       44070 if(!$sth) {
708 28         221 $sth = $dbh->prepare($sql);
709             }
710 120         5221 $sth->execute(@bind);
711 120         1028 my $result = $sth->fetchall_arrayref();
712 120 50       329 if(!defined($result)) {
713 0         0 confess "Statement handle is inactive. Something is wrong.";
714             }
715 120 100       261 if(@$result) {
716 67         242 push @ret_contained, $query_elem;
717             }else {
718 53         199 push @ret_not_contained, $query_elem;
719             }
720             }
721 35         1313 return (undef, \@ret_contained, \@ret_not_contained);
722             }catch {
723 0     0   0 my $e = shift;
724 0         0 return ($e);
725 39         495 };
726 39         1176 @_ = @method_result;
727 39         246 goto $callback;
728             }
729              
730             sub get_timeline_names {
731 10     10 1 4215 my ($self) = @_;
732 10         47 my $dbh = $self->_get_my_dbh();
733 10         75 my ($sql, @bind) = $self->{maker}->select(
734             'timelines', ['name']
735             );
736 10         2594 my $result = $dbh->selectall_arrayref($sql, undef, @bind);
737 10         2088 my @return = map { $_->[0] } @$result;
  16         60  
738 10         413 return @return;
739             }
740              
741             1;
742              
743             __END__