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   817 use strict;
  3         5  
  3         68  
2 3     3   11 use warnings;
  3         4  
  3         123  
3             # ABSTRACT: construct and execute a complex query
4              
5             #pod =head1 DESCRIPTION
6             #pod
7             #pod Rubric::Entry::Query builds a query based on a simple hash of parameters,
8             #pod performs that query, and returns the rendered report on the results.
9             #pod
10             #pod =cut
11              
12             use Date::Span;
13 3     3   607 use Digest::MD5 qw(md5_hex);
  3         1288  
  3         156  
14 3     3   17  
  3         4  
  3         89  
15             use Rubric::Config;
16 3     3   88  
  3         8  
  3         20  
17             #pod =head1 METHODS
18             #pod
19             #pod =head2 query(\%arg, \%context)
20             #pod
21             #pod This is the only interface to this module. Given a hashref of named arguments,
22             #pod it returns the entries that match constraints built from the arguments. It
23             #pod generates these constraints with C<get_constraint> and its helpers. If any
24             #pod constraint is invalid, an empty set of results is returned.
25             #pod
26             #pod The second hashref passed to the method provides context for generating
27             #pod implicit query parameters; for example, if the querying user is indicated in
28             #pod the context, private entries for that user will be returned.
29             #pod
30             #pod =cut
31              
32             my ($self, $user) = @_;
33             my $priv_tag = Rubric::Config->private_tag;
34 32     32   118 $priv_tag = Rubric::Entry->db_Main->quote($priv_tag);
35 32         227  
36 32         93 return "id NOT IN (SELECT entry FROM entrytags WHERE tag=$priv_tag)"
37             unless $user;
38 32 100       1377  
39             $user = Rubric::Entry->db_Main->quote($user);
40             return
41 10         826 "((username = $user) OR " .
42             "id NOT IN (SELECT entry FROM entrytags WHERE tag=$priv_tag))";
43 10         2715 }
44              
45             return q{id NOT IN (SELECT entry FROM entrytags WHERE tag='@nolist')};
46             }
47              
48 47     47   92 my ($self, $arg, $context) = @_;
49             $context ||= {};
50              
51             my @constraints = map { $self->get_constraint($_, $arg->{$_}) } keys %$arg;
52 47     47 1 169 @constraints = ("1 = 0") if grep { not defined } @constraints;
53 47   100     139  
54             push @constraints, $self->_nolist_constraint;
55 47         146 push @constraints, $self->_private_constraint($context->{user})
  59         1013  
56 47 100       3878 if exists $context->{user};
  58         179  
57              
58 47         132 ## no critic (ConditionalDeclarations)
59             my $order_by = "$context->{order_by} DESC"
60 47 100       195 if $context->{order_by}||'' =~ /\A(?:created|modified)\Z/;
61              
62             $self->get_entries(\@constraints, $order_by);
63             }
64 47 50 33     522  
65             #pod =head2 get_constraint($param => $value)
66 47         180 #pod
67             #pod Given a name/value pair describing a constraint, this method will attempt to
68             #pod generate part of an SQL WHERE clause enforcing the constraint. To do this, it
69             #pod looks for and calls a method called "constraint_for_NAME" where NAME is the
70             #pod passed value of C<$param>. If no clause can be generated, it returns undef.
71             #pod
72             #pod =cut
73              
74             my ($self, $param, $value) = @_;
75              
76             ## no critic (ReturnUndef)
77             return undef unless my $code = $self->can("constraint_for_$param");
78             $code->($self, $value);
79 59     59 1 107 }
80              
81             #pod =head2 get_entries(\@constraints)
82 59 100       328 #pod
83 58         168 #pod Given a set of SQL constraints, this method builds the WHERE and ORDER BY
84             #pod clauses and performs a query with Class::DBI's C<retrieve_from_sql>.
85             #pod
86             #pod =cut
87              
88             my ($self, $constraints, $order_by) = @_;
89             $order_by ||= 'created DESC';
90             return Rubric::Entry->retrieve_all unless @$constraints;
91             Rubric::Entry->retrieve_from_sql(
92             join(" AND ", @$constraints)
93             . " ORDER BY $order_by"
94 47     47 1 108 );
95 47   50     144 }
96 47 50       125  
97 47         319 #pod =head2 constraint_for_NAME
98             #pod
99             #pod These methods are called to produce SQL for the named parameter, and are passed
100             #pod a scalar argument. If the argument is not valid, they return undef, which will
101             #pod cause C<query> to produce an empty set of records.
102             #pod
103             #pod =head3 constraint_for_user($user)
104             #pod
105             #pod Given a Rubric::User object, this returns SQL to limit results to entries by
106             #pod the user.
107             #pod
108             #pod =cut
109              
110             my ($self, $user) = @_;
111             ## no critic (ReturnUndef)
112             return undef unless $user;
113             return "username = " . Rubric::Entry->db_Main->quote($user);
114             }
115              
116             #pod =head3 constraint_for_tags($tags)
117 13     13 1 27 #pod
118             #pod =head3 constraint_for_exact_tags($tags)
119 13 100       36 #pod
120 12         1034 #pod Given a set of tags, this returns SQL to limit results to entries marked
121             #pod with the given tags.
122             #pod
123             #pod The C<exact> version of this constraint returns SQL for entries with only the
124             #pod given tags.
125             #pod
126             #pod =cut
127              
128             my ($self, $tags) = @_;
129              
130             ## no critic (ReturnUndef)
131             return undef unless $tags and ref $tags eq 'HASH';
132             ## use critic
133             return unless %$tags;
134              
135             my @snippets;
136 20     20 1 41 while (my ($tag, $tag_value) = each %$tags) {
137             my $tn = Rubric::Entry->db_Main->quote($tag);
138             my $tv = Rubric::Entry->db_Main->quote($tag_value);
139 20 100 100     94 push @snippets, defined $tag_value
140             ? "id IN (SELECT entry FROM entrytags WHERE tag=$tn AND tag_value=$tv)"
141 18 100       50 : "id IN (SELECT entry FROM entrytags WHERE tag=$tn)"
142             }
143 16         23  
144 16         63 return join ' AND ', @snippets;
145 16         65 }
146 16         1224  
147 16 50       544 my ($self, $tags) = @_;
148              
149             ## no critic (ReturnUndef)
150             return undef unless $tags and ref $tags eq 'HASH';
151             ## use critic
152 16         76  
153             my $count = keys %$tags;
154              
155             # XXX determine which one is faster
156 1     1 1 2 return
157             "(SELECT COUNT(tag) FROM entrytags WHERE entry = entries.id) = $count",
158             # "id IN (SELECT entry FROM entrytags GROUP BY entry HAVING COUNT(tag) = $count)",
159 1 50 33     7 $self->constraint_for_tags($tags);
160             }
161              
162 1         2 #pod =head3 constraint_for_desc_like($value)
163             #pod
164             #pod =cut
165              
166 1         3 my ($self, $value) = @_;
167             my $like = substr Rubric::Entry->db_Main->quote($value), 1, -1;
168             "(description LIKE '\%$like\%' OR title LIKE '\%$like\%')"
169             }
170              
171             #pod =head3 constraint_for_body_like($value)
172             #pod
173             #pod =cut
174              
175             my ($self, $value) = @_;
176 2     2 1 4 my $like = substr Rubric::Entry->db_Main->quote($value), 1, -1;
177 2         5 "(body LIKE '\%$like\%')"
178 2         100 }
179              
180             #pod =head3 constraint_for_like($value)
181             #pod
182             #pod =cut
183              
184             my ($self, $value) = @_;
185             "(" . $self->constraint_for_desc_like($value) .
186 2     2 1 3 "OR" . $self->constraint_for_body_like($value) . ")"
187 2         5 }
188 2         79  
189             #pod =head3 constraint_for_has_body($bool)
190             #pod
191             #pod This returns SQL to limit the results to entries with bodies.
192             #pod
193             #pod =cut
194              
195             my ($self, $bool) = @_;
196 1     1 1 2 return $bool ? "body IS NOT NULL" : "body IS NULL";
197 1         3 }
198              
199             #pod =head3 constraint_for_has_link($bool)
200             #pod
201             #pod This returns SQL to limit the results to entries with links.
202             #pod
203             #pod =cut
204              
205             my ($self, $bool) = @_;
206             return $bool ? "link IS NOT NULL" : "link IS NULL";
207             }
208 2     2 1 2  
209 2 100       6 #pod =head3 constraint_for_first_only($bool)
210             #pod
211             #pod This returns SQL to limit the results to the first entry posted for any given
212             #pod link.
213             #pod
214             #pod =cut
215              
216             my ($self, $bool) = @_;
217             return $bool
218             ? "(link is NULL OR id IN (SELECT MIN(id) FROM entries GROUP BY link))"
219 2     2 1 4 : ();
220 2 100       6 }
221              
222             #pod =head3 constraint_for_urimd5($md5)
223             #pod
224             #pod This returns SQL to limit the results to entries whose link has the given
225             #pod md5sum.
226             #pod
227             #pod =cut
228              
229             my ($self, $md5) = @_;
230             ## no critic (ReturnUndef)
231 10     10 1 20 return undef unless my ($link) = Rubric::Link->search({ md5 => $md5 });
232 10 50       61 ## use critic
233              
234             return "link = " . $link->id;
235             }
236              
237             #pod =head3 constraint_for_{timefield}_{preposition}($datetime)
238             #pod
239             #pod This set of six methods return SQL to limit the results based on its
240             #pod timestamps.
241             #pod
242             #pod The passed value is a complete or partial datetime in the form:
243             #pod
244             #pod YYYY[-MM[-DD[ HH[:MM]]]] # space may be replaced with 'T'
245 4     4 1 10 #pod
246             #pod The timefield may be "created" or "modified".
247 4 100       32 #pod
248             #pod The prepositions are as follows:
249             #pod
250 3         3007 #pod after - after the latest part of the given unit of time
251             #pod before - before the earliest part of the given unit of time
252             #pod on - after (or at) the earliest part and before (or at) the latest part
253             #pod
254             #pod =cut
255              
256             ## here there be small lizards
257             ## date parameter handling below...
258              
259             my ($datetime) = @_;
260             return unless my @unit = $datetime =~
261             qr/^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:(?:T|)(\d{2})(?::(\d{2}))?)?)?)?$/o;
262             $unit[1]-- if $unit[1];
263             return @unit;
264             }
265              
266             {
267             ## no critic (NoStrict)
268             no strict 'refs';
269             for my $field (qw(created modified)) {
270             for my $prep (qw(after before on)) {
271             *{"constraint_for_${field}_${prep}"} = sub {
272             my ($self, $datetime) = @_;
273             ## no critic (ReturnUndef)
274             return undef unless my @time = _unit_from_string($datetime);
275             ## use critic
276 4     4   5  
277 4 100       34 my ($start,$end) = range_from_unit(@time);
278             return
279 3 100       11 ( $prep eq 'after' ? "$field > $end"
280 3         14 : $prep eq 'before' ? "$field < $start"
281             : "$field >= $start AND $field <= $end")
282             # : $prep eq 'on' ? "$field >= $start AND $field <= $end"
283             # : die "illegal preposition in temporal comparison" )
284             }
285 3     3   20 }
  3         4  
  3         452  
286             }
287             }
288              
289 4     4   5 1;
290              
291 4 100       7  
292             =pod
293              
294 3         7 =encoding UTF-8
295              
296 3 100       136 =head1 NAME
    100          
297              
298             Rubric::Entry::Query - construct and execute a complex query
299              
300             =head1 VERSION
301              
302             version 0.157
303              
304             =head1 DESCRIPTION
305              
306             Rubric::Entry::Query builds a query based on a simple hash of parameters,
307             performs that query, and returns the rendered report on the results.
308              
309             =head1 PERL VERSION
310              
311             This code is effectively abandonware. Although releases will sometimes be made
312             to update contact info or to fix packaging flaws, bug reports will mostly be
313             ignored. Feature requests are even more likely to be ignored. (If someone
314             takes up maintenance of this code, they will presumably remove this notice.)
315             This means that whatever version of perl is currently required is unlikely to
316             change -- but also that it might change at any new maintainer's whim.
317              
318             =head1 METHODS
319              
320             =head2 query(\%arg, \%context)
321              
322             This is the only interface to this module. Given a hashref of named arguments,
323             it returns the entries that match constraints built from the arguments. It
324             generates these constraints with C<get_constraint> and its helpers. If any
325             constraint is invalid, an empty set of results is returned.
326              
327             The second hashref passed to the method provides context for generating
328             implicit query parameters; for example, if the querying user is indicated in
329             the context, private entries for that user will be returned.
330              
331             =head2 get_constraint($param => $value)
332              
333             Given a name/value pair describing a constraint, this method will attempt to
334             generate part of an SQL WHERE clause enforcing the constraint. To do this, it
335             looks for and calls a method called "constraint_for_NAME" where NAME is the
336             passed value of C<$param>. If no clause can be generated, it returns undef.
337              
338             =head2 get_entries(\@constraints)
339              
340             Given a set of SQL constraints, this method builds the WHERE and ORDER BY
341             clauses and performs a query with Class::DBI's C<retrieve_from_sql>.
342              
343             =head2 constraint_for_NAME
344              
345             These methods are called to produce SQL for the named parameter, and are passed
346             a scalar argument. If the argument is not valid, they return undef, which will
347             cause C<query> to produce an empty set of records.
348              
349             =head3 constraint_for_user($user)
350              
351             Given a Rubric::User object, this returns SQL to limit results to entries by
352             the user.
353              
354             =head3 constraint_for_tags($tags)
355              
356             =head3 constraint_for_exact_tags($tags)
357              
358             Given a set of tags, this returns SQL to limit results to entries marked
359             with the given tags.
360              
361             The C<exact> version of this constraint returns SQL for entries with only the
362             given tags.
363              
364             =head3 constraint_for_desc_like($value)
365              
366             =head3 constraint_for_body_like($value)
367              
368             =head3 constraint_for_like($value)
369              
370             =head3 constraint_for_has_body($bool)
371              
372             This returns SQL to limit the results to entries with bodies.
373              
374             =head3 constraint_for_has_link($bool)
375              
376             This returns SQL to limit the results to entries with links.
377              
378             =head3 constraint_for_first_only($bool)
379              
380             This returns SQL to limit the results to the first entry posted for any given
381             link.
382              
383             =head3 constraint_for_urimd5($md5)
384              
385             This returns SQL to limit the results to entries whose link has the given
386             md5sum.
387              
388             =head3 constraint_for_{timefield}_{preposition}($datetime)
389              
390             This set of six methods return SQL to limit the results based on its
391             timestamps.
392              
393             The passed value is a complete or partial datetime in the form:
394              
395             YYYY[-MM[-DD[ HH[:MM]]]] # space may be replaced with 'T'
396              
397             The timefield may be "created" or "modified".
398              
399             The prepositions are as follows:
400              
401             after - after the latest part of the given unit of time
402             before - before the earliest part of the given unit of time
403             on - after (or at) the earliest part and before (or at) the latest part
404              
405             =head1 AUTHOR
406              
407             Ricardo SIGNES <rjbs@semiotic.systems>
408              
409             =head1 COPYRIGHT AND LICENSE
410              
411             This software is copyright (c) 2004 by Ricardo SIGNES.
412              
413             This is free software; you can redistribute it and/or modify it under
414             the same terms as the Perl 5 programming language system itself.
415              
416             =cut