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