File Coverage

blib/lib/Rubric/Entry/Query.pm
Criterion Covered Total %
statement 83 83 100.0
branch 35 40 87.5
condition 8 13 61.5
subroutine 23 23 100.0
pod 13 13 100.0
total 162 172 94.1


line stmt bran cond sub pod time code
1 3     3   1191 use strict;
  3         9  
  3         86  
2 3     3   17 use warnings;
  3         7  
  3         219  
3             package Rubric::Entry::Query;
4             # ABSTRACT: construct and execute a complex query
5             $Rubric::Entry::Query::VERSION = '0.156';
6             #pod =head1 DESCRIPTION
7             #pod
8             #pod Rubric::Entry::Query builds a query based on a simple hash of parameters,
9             #pod performs that query, and returns the rendered report on the results.
10             #pod
11             #pod =cut
12              
13 3     3   1372 use Date::Span;
  3         1519  
  3         198  
14 3     3   16 use Digest::MD5 qw(md5_hex);
  3         7  
  3         123  
15              
16 3     3   15 use Rubric::Config;
  3         6  
  3         26  
17              
18             #pod =head1 METHODS
19             #pod
20             #pod =head2 query(\%arg, \%context)
21             #pod
22             #pod This is the only interface to this module. Given a hashref of named arguments,
23             #pod it returns the entries that match constraints built from the arguments. It
24             #pod generates these constraints with C and its helpers. If any
25             #pod constraint is invalid, an empty set of results is returned.
26             #pod
27             #pod The second hashref passed to the method provides context for generating
28             #pod implicit query parameters; for example, if the querying user is indicated in
29             #pod the context, private entries for that user will be returned.
30             #pod
31             #pod =cut
32              
33             sub _private_constraint {
34 32     32   56 my ($self, $user) = @_;
35 32         157 my $priv_tag = Rubric::Config->private_tag;
36 32         140 $priv_tag = Rubric::Entry->db_Main->quote($priv_tag);
37              
38 32 100       1757 return "id NOT IN (SELECT entry FROM entrytags WHERE tag=$priv_tag)"
39             unless $user;
40              
41 10         593 $user = Rubric::Entry->db_Main->quote($user);
42             return
43 10         2388 "((username = $user) OR " .
44             "id NOT IN (SELECT entry FROM entrytags WHERE tag=$priv_tag))";
45             }
46              
47             sub _nolist_constraint {
48 47     47   117 return q{id NOT IN (SELECT entry FROM entrytags WHERE tag='@nolist')};
49             }
50              
51             sub query {
52 47     47 1 98 my ($self, $arg, $context) = @_;
53 47   100     170 $context ||= {};
54              
55 47         180 my @constraints = map { $self->get_constraint($_, $arg->{$_}) } keys %$arg;
  59         930  
56 47 100       3106 @constraints = ("1 = 0") if grep { not defined } @constraints;
  58         223  
57              
58 47         166 push @constraints, $self->_nolist_constraint;
59             push @constraints, $self->_private_constraint($context->{user})
60 47 100       222 if exists $context->{user};
61              
62             ## no critic (ConditionalDeclarations)
63             my $order_by = "$context->{order_by} DESC"
64 47 50 33     331 if $context->{order_by}||'' =~ /\A(?:created|modified)\Z/;
65              
66 47         145 $self->get_entries(\@constraints, $order_by);
67             }
68              
69             #pod =head2 get_constraint($param => $value)
70             #pod
71             #pod Given a name/value pair describing a constraint, this method will attempt to
72             #pod generate part of an SQL WHERE clause enforcing the constraint. To do this, it
73             #pod looks for and calls a method called "constraint_for_NAME" where NAME is the
74             #pod passed value of C<$param>. If no clause can be generated, it returns undef.
75             #pod
76             #pod =cut
77              
78             sub get_constraint {
79 59     59 1 114 my ($self, $param, $value) = @_;
80              
81             ## no critic (ReturnUndef)
82 59 100       400 return undef unless my $code = $self->can("constraint_for_$param");
83 58         259 $code->($self, $value);
84             }
85              
86             #pod =head2 get_entries(\@constraints)
87             #pod
88             #pod Given a set of SQL constraints, this method builds the WHERE and ORDER BY
89             #pod clauses and performs a query with Class::DBI's C.
90             #pod
91             #pod =cut
92              
93             sub get_entries {
94 47     47 1 85 my ($self, $constraints, $order_by) = @_;
95 47   50     171 $order_by ||= 'created DESC';
96 47 50       105 return Rubric::Entry->retrieve_all unless @$constraints;
97 47         354 Rubric::Entry->retrieve_from_sql(
98             join(" AND ", @$constraints)
99             . " ORDER BY $order_by"
100             );
101             }
102              
103             #pod =head2 constraint_for_NAME
104             #pod
105             #pod These methods are called to produce SQL for the named parameter, and are passed
106             #pod a scalar argument. If the argument is not valid, they return undef, which will
107             #pod cause C to produce an empty set of records.
108             #pod
109             #pod =head3 constraint_for_user($user)
110             #pod
111             #pod Given a Rubric::User object, this returns SQL to limit results to entries by
112             #pod the user.
113             #pod
114             #pod =cut
115              
116             sub constraint_for_user {
117 13     13 1 28 my ($self, $user) = @_;
118             ## no critic (ReturnUndef)
119 13 100       39 return undef unless $user;
120 12         742 return "username = " . Rubric::Entry->db_Main->quote($user);
121             }
122              
123             #pod =head3 constraint_for_tags($tags)
124             #pod
125             #pod =head3 constraint_for_exact_tags($tags)
126             #pod
127             #pod Given a set of tags, this returns SQL to limit results to entries marked
128             #pod with the given tags.
129             #pod
130             #pod The C version of this constraint returns SQL for entries with only the
131             #pod given tags.
132             #pod
133             #pod =cut
134              
135             sub constraint_for_tags {
136 20     20 1 45 my ($self, $tags) = @_;
137              
138             ## no critic (ReturnUndef)
139 20 100 100     151 return undef unless $tags and ref $tags eq 'HASH';
140             ## use critic
141 18 100       58 return unless %$tags;
142              
143 16         27 my @snippets;
144 16         77 while (my ($tag, $tag_value) = each %$tags) {
145 16         111 my $tn = Rubric::Entry->db_Main->quote($tag);
146 16         1551 my $tv = Rubric::Entry->db_Main->quote($tag_value);
147 16 50       853 push @snippets, defined $tag_value
148             ? "id IN (SELECT entry FROM entrytags WHERE tag=$tn AND tag_value=$tv)"
149             : "id IN (SELECT entry FROM entrytags WHERE tag=$tn)"
150             }
151              
152 16         94 return join ' AND ', @snippets;
153             }
154              
155             sub constraint_for_exact_tags {
156 1     1 1 3 my ($self, $tags) = @_;
157              
158             ## no critic (ReturnUndef)
159 1 50 33     10 return undef unless $tags and ref $tags eq 'HASH';
160             ## use critic
161              
162 1         2 my $count = keys %$tags;
163              
164             # XXX determine which one is faster
165             return
166 1         5 "(SELECT COUNT(tag) FROM entrytags WHERE entry = entries.id) = $count",
167             # "id IN (SELECT entry FROM entrytags GROUP BY entry HAVING COUNT(tag) = $count)",
168             $self->constraint_for_tags($tags);
169             }
170              
171             #pod =head3 constraint_for_desc_like($value)
172             #pod
173             #pod =cut
174              
175             sub constraint_for_desc_like {
176 2     2 1 4 my ($self, $value) = @_;
177 2         9 my $like = substr Rubric::Entry->db_Main->quote($value), 1, -1;
178 2         120 "(description LIKE '\%$like\%' OR title LIKE '\%$like\%')"
179             }
180              
181             #pod =head3 constraint_for_body_like($value)
182             #pod
183             #pod =cut
184              
185             sub constraint_for_body_like {
186 2     2 1 4 my ($self, $value) = @_;
187 2         8 my $like = substr Rubric::Entry->db_Main->quote($value), 1, -1;
188 2         109 "(body LIKE '\%$like\%')"
189             }
190              
191             #pod =head3 constraint_for_like($value)
192             #pod
193             #pod =cut
194              
195             sub constraint_for_like {
196 1     1 1 3 my ($self, $value) = @_;
197 1         5 "(" . $self->constraint_for_desc_like($value) .
198             "OR" . $self->constraint_for_body_like($value) . ")"
199             }
200              
201             #pod =head3 constraint_for_has_body($bool)
202             #pod
203             #pod This returns SQL to limit the results to entries with bodies.
204             #pod
205             #pod =cut
206              
207             sub constraint_for_has_body {
208 2     2 1 5 my ($self, $bool) = @_;
209 2 100       11 return $bool ? "body IS NOT NULL" : "body IS NULL";
210             }
211              
212             #pod =head3 constraint_for_has_link($bool)
213             #pod
214             #pod This returns SQL to limit the results to entries with links.
215             #pod
216             #pod =cut
217              
218             sub constraint_for_has_link {
219 2     2 1 4 my ($self, $bool) = @_;
220 2 100       12 return $bool ? "link IS NOT NULL" : "link IS NULL";
221             }
222              
223             #pod =head3 constraint_for_first_only($bool)
224             #pod
225             #pod This returns SQL to limit the results to the first entry posted for any given
226             #pod link.
227             #pod
228             #pod =cut
229              
230             sub constraint_for_first_only {
231 10     10 1 22 my ($self, $bool) = @_;
232 10 50       74 return $bool
233             ? "(link is NULL OR id IN (SELECT MIN(id) FROM entries GROUP BY link))"
234             : ();
235             }
236              
237             #pod =head3 constraint_for_urimd5($md5)
238             #pod
239             #pod This returns SQL to limit the results to entries whose link has the given
240             #pod md5sum.
241             #pod
242             #pod =cut
243              
244             sub constraint_for_urimd5 {
245 4     4 1 8 my ($self, $md5) = @_;
246             ## no critic (ReturnUndef)
247 4 100       48 return undef unless my ($link) = Rubric::Link->search({ md5 => $md5 });
248             ## use critic
249              
250 3         2843 return "link = " . $link->id;
251             }
252              
253             #pod =head3 constraint_for_{timefield}_{preposition}($datetime)
254             #pod
255             #pod This set of six methods return SQL to limit the results based on its
256             #pod timestamps.
257             #pod
258             #pod The passed value is a complete or partial datetime in the form:
259             #pod
260             #pod YYYY[-MM[-DD[ HH[:MM]]]] # space may be replaced with 'T'
261             #pod
262             #pod The timefield may be "created" or "modified".
263             #pod
264             #pod The prepositions are as follows:
265             #pod
266             #pod after - after the latest part of the given unit of time
267             #pod before - before the earliest part of the given unit of time
268             #pod on - after (or at) the earliest part and before (or at) the latest part
269             #pod
270             #pod =cut
271              
272             ## here there be small lizards
273             ## date parameter handling below...
274              
275             sub _unit_from_string {
276 4     4   8 my ($datetime) = @_;
277 4 100       51 return unless my @unit = $datetime =~
278             qr/^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:(?:T|)(\d{2})(?::(\d{2}))?)?)?)?$/o;
279 3 100       14 $unit[1]-- if $unit[1];
280 3         16 return @unit;
281             }
282              
283             {
284             ## no critic (NoStrict)
285 3     3   19 no strict 'refs';
  3         6  
  3         584  
286             for my $field (qw(created modified)) {
287             for my $prep (qw(after before on)) {
288             *{"constraint_for_${field}_${prep}"} = sub {
289 4     4   17 my ($self, $datetime) = @_;
290             ## no critic (ReturnUndef)
291 4 100       10 return undef unless my @time = _unit_from_string($datetime);
292             ## use critic
293              
294 3         10 my ($start,$end) = range_from_unit(@time);
295             return
296 3 100       167 ( $prep eq 'after' ? "$field > $end"
    100          
297             : $prep eq 'before' ? "$field < $start"
298             : "$field >= $start AND $field <= $end")
299             # : $prep eq 'on' ? "$field >= $start AND $field <= $end"
300             # : die "illegal preposition in temporal comparison" )
301             }
302             }
303             }
304             }
305              
306             1;
307              
308             __END__