File Coverage

lib/DBIx/EAV/Cursor.pm
Criterion Covered Total %
statement 167 177 94.3
branch 77 94 81.9
condition 4 9 44.4
subroutine 12 14 85.7
pod 4 5 80.0
total 264 299 88.2


line stmt bran cond sub pod time code
1             package DBIx::EAV::Cursor;
2              
3 10     10   31 use Moo;
  10         10  
  10         40  
4 10     10   1618 use Carp qw/ croak confess /;
  10         16  
  10         434  
5 10     10   33 use Data::Dumper;
  10         15  
  10         303  
6 10     10   6620 use SQL::Abstract;
  10         73159  
  10         512  
7 10     10   57 use Scalar::Util qw/ blessed /;
  10         11  
  10         16814  
8              
9             my $sql = SQL::Abstract->new();
10              
11             has 'eav', is => 'ro', required => 1;
12             has 'type', is => 'ro', required => 1;
13              
14             has 'query', is => 'ro', default => sub { {} };
15             has 'options', is => 'ro', default => sub { {} };
16              
17             has '_sth', is => 'ro', lazy => 1, builder => 1, predicate => '_has_sth', clearer => '_reset_sth';
18              
19              
20             sub _check_query_already_executed {
21 0     0   0 my $self = shift;
22 0 0       0 croak "Query already executed!" if defined $self->_sth;
23             }
24              
25              
26             sub _build__sth {
27 94     94   6433 my $self = shift;
28 94         163 my ($sql_query, $bind) = $self->_build_sql_query();
29 94         1748 my ($rv, $sth) = $self->eav->table('entities')->_do($sql_query, $bind);
30 94         1620 $sth;
31             }
32              
33              
34             sub _build_sql_query {
35 100     100   80 my $self = shift;
36              
37 100         129 my $opts = $self->options;
38 100         115 my $eav = $self->eav;
39 100         106 my $type = $self->type;
40 100         1415 my $entities_table = $eav->table('entities');
41 100         87 my ($order_by, $group_by, $having, %parser_data, %replacements);
42              
43             # selected field
44 20         35 my @select_fields = $opts->{select} ? @{$opts->{select}}
45 100 100       173 : @{$entities_table->columns};
  80         216  
46              
47             # distinct (before normalizing @select_fields)
48 100 100       184 if ($opts->{distinct}) {
49              
50             # if has group_by, warn and ignore distinct
51 3 50       7 if ($opts->{group_by}) {
52              
53             }
54             else {
55             # exclude id from group by to make the distinct effect
56 3   66     6 $opts->{group_by} = [grep { !ref && $_ !~ /^(me\.|)id$/ } @select_fields];
  5         29  
57             }
58             }
59              
60             # normalize select fields
61 100         206 for (my $i = 0; $i < @select_fields; $i++) {
62              
63             # literal, dont touch
64 230 100       404 next if ref $select_fields[$i] eq 'SCALAR';
65              
66 216         172 my $ident = $select_fields[$i];
67 216         166 my ($fn, $as);
68              
69             # sql function
70 216 100       284 if (ref $ident) {
71              
72 2         7 $as = delete $ident->{'-as'};
73 2         5 ($fn, $ident) = each %$ident;
74              
75 2 50       8 unless ($as) {
76 2         5 $as = lc($fn.'_'.$ident);
77 2         5 $as =~ s/\./_/g;
78             }
79              
80 2         5 $parser_data{aliases}{$as} = 1;
81             }
82              
83 216         342 my $info = $self->_parse_clause_identifier($ident, \%parser_data);
84              
85             $select_fields[$i] = $fn ? \(sprintf "%s( %s ) AS %s", uc $fn, $info->{replacement}, $as)
86 216 100       720 : $info->{replacement};
87             }
88              
89             # add type criteria unless we have a subselect ('from' option)
90 100 100       281 my $type_criteria = $opts->{from} ? [] : [ entity_type_id => $type->id ];
91              
92 100 100       164 if ($opts->{subtype_depth}) {
93             push @$type_criteria, [ '_parent_type_'.$_, $type->id ]
94 3         37 for 1 .. $opts->{subtype_depth};
95             }
96              
97             # parse WHERE
98 100         429 my ($where, @bind) = $sql->where({ -and => [ $type_criteria, $self->query] });
99 100         41470 my $i = 0;
100              
101 100         228 my $where_re = qr/ ([\w._]+) (?:=|!=|<>|>|<|>=|<=|IN|IS NULL|LIKE|NOT LIKE) \?/;
102 100         696 while ($where =~ /$where_re/g) {
103              
104 176         280 my $ident = $1;
105 176         309 my $info = $self->_parse_clause_identifier($ident, \%parser_data, $bind[$i]);
106              
107             $bind[$i] = $info->{bind}
108 176 100       327 if exists $info->{bind};
109              
110 176         257 $replacements{$ident} = $info->{replacement};
111              
112 176         758 $i++;
113             }
114              
115             # replace identifiers in WHERE
116 100         242 while (my ($string, $replacement) = each %replacements) {
117 173         2008 $where =~ s/\b$string\b/$replacement/g;
118             }
119              
120             # parse ORDER BY
121 100 100       188 if (defined $opts->{order_by}) {
122              
123 10         25 %replacements = ();
124 10         39 $order_by = $sql->where(undef, $opts->{order_by});
125              
126 10         1488 while ($order_by =~ / ([\w._]+)(?: ASC| DESC|,|$)/g) {
127              
128 11         25 my $ident = $1;
129 11         29 my $info = $self->_parse_clause_identifier($ident, \%parser_data);
130              
131             die "Cursor: query error: can't order by relationship! ($ident)'"
132 11 50       33 if $info->{is_relationship};
133              
134 11         42 $replacements{$ident} = $info->{replacement};
135             }
136              
137             # replace identifiers
138 10         43 while (my ($string, $replacement) = each %replacements) {
139 11         157 $order_by =~ s/\b$string\b/$replacement/g;
140             }
141             }
142              
143             # prepare prefetch attributes
144             # if ($opts->{prefetch}) {
145             # foreach my $attr (ref $opts->{prefetch} eq 'ARRAY' ? @{$opts->{prefetch}} : ($opts->{prefetch})) {
146             # die "unknown attribute given to prefetch: '$attr'"
147             # unless $attr =~ /^(?:$possible_attrs)$/;
148             #
149             # $join_attr{$attr} = 1;
150             # push @select_fields, "$attr.value AS $attr";
151             # }
152             # }
153              
154             # parse ORDER BY
155 100 100       151 if (defined $opts->{group_by}) {
156              
157 7         8 my @fields;
158              
159 7 100       20 foreach my $ident (ref $opts->{group_by} eq 'ARRAY' ? @{$opts->{group_by}} : $opts->{group_by}) {
  6         23  
160              
161 9         15 my $info = $self->_parse_clause_identifier($ident, \%parser_data);
162              
163             die "Cursor: query error: can't group by a relationship! ($ident)'"
164 9 50       19 if $info->{is_relationship};
165              
166 9         19 push @fields, $info->{replacement};
167             }
168              
169 7         19 $group_by .= 'GROUP BY '. join(', ', @fields);
170             }
171              
172             # parse HAVING
173 100 100       138 if (defined $opts->{having}) {
174              
175 2         3 my @having_bind;
176 2         7 ($having, @having_bind) = $sql->where($opts->{having});
177              
178 2         345 push @bind, @having_bind;
179 2         11 $having =~ s/^\s*WHERE/HAVING/;
180              
181 2         4 %replacements = ();
182 2         15 while ($having =~ /$where_re/g) {
183              
184 2         4 my $ident = $1;
185 2         6 my $info = $self->_parse_clause_identifier($ident, \%parser_data);
186              
187 2         11 $replacements{$ident} = $info->{replacement};
188             }
189              
190             # replace identifiers
191 2         8 while (my ($string, $replacement) = each %replacements) {
192 2         30 $having =~ s/\b$string\b/$replacement/g;
193             }
194             }
195              
196             # build sql statement
197              
198             # SELECT ... FROM
199              
200             # from subselect
201 100         186 my $from = $entities_table->name;
202 100 100       154 if (my $subquery = $opts->{from}) {
203              
204 3         4 my ($sub_select, $sub_bind) = @$$subquery;
205 3         6 $from = "($sub_select)";
206 3         5 push @bind, @$sub_bind;
207             }
208              
209 100         317 my $sql_query = $sql->select("$from AS me", \@select_fields);
210              
211             # JOINs
212 100 100       8612 if (my $depth = $opts->{subtype_depth}) {
213              
214 3         84 my $hierarchy_table = $eav->table("type_hierarchy")->name;
215 3         12 $sql_query .= " LEFT JOIN $hierarchy_table AS _parent_type_1 ON (_parent_type_1.child_type_id = me.entity_type_id)";
216 3         6 my $i = 2;
217 3         12 while ($depth > 1) {
218 3         18 $sql_query .= sprintf(" LEFT JOIN $hierarchy_table AS _parent_type_%d ON (_parent_type_%d.child_type_id = _parent_type_%d.parent_type_id)",
219             $i, $i, $i - 1);
220 3         3 $depth--;
221 3         10 $i++;
222             }
223             }
224              
225 100 100       92 $sql_query .= " $_" for @{$parser_data{joins} || []};
  100         430  
226              
227             # WHERE, GROUP BY, HAVING, ORDER BY
228 100         173 $sql_query .= " $where";
229 100 100       146 $sql_query .= " $group_by" if defined $group_by;
230 100 100       146 $sql_query .= " $having" if defined $having;
231 100 100       142 $sql_query .= " $order_by" if defined $order_by;
232              
233             # LIMIT / OFFSET
234 100 100       155 if ($opts->{limit}) {
235 2 50       8 die "invalid limit" unless $opts->{limit} =~ /^\d+$/;
236 2         4 $sql_query .= " LIMIT $opts->{limit}";
237              
238 2 50       3 if (defined $opts->{offset}) {
239 2 50       7 die "invalid offset" unless $opts->{offset} =~ /^\d+$/;
240 2         3 $sql_query .= " OFFSET $opts->{offset}";
241             }
242             }
243              
244             # return query and bind values
245 100         666 ($sql_query, \@bind);
246             }
247              
248             sub _parse_clause_identifier {
249 414     414   459 my ($self, $identifier, $parser_data, $bind_value) = @_;
250              
251             # cached
252             return $parser_data->{cache}->{$identifier}
253 414 100       811 if exists $parser_data->{cache}->{$identifier};
254              
255 328         381 my $type = $self->type;
256 328         315 my $eav = $self->eav;
257              
258             # special case: parent_type
259 328 100       529 return $parser_data->{cache}->{$identifier} = { replacement => $identifier.'.parent_type_id' }
260             if $identifier =~ /^_parent_type_\d+$/;
261              
262             # special case: alias
263             return { replacement => $identifier }
264 322 100       461 if exists $parser_data->{aliases}{$identifier};
265              
266             # remove me.
267 319         311 $identifier =~ s/^me\.//;
268              
269             # parse possibly deep related identifier
270             # valid formats:
271             # -
272             # -
273             # - +.
274              
275 319         579 my @parts = split /\./, $identifier;
276 319         217 my @joins;
277 319         239 my $current_type = $type;
278 319         238 my $current_entity_alias = 'me';
279 319         211 my @rels;
280              
281 319         717 for (my $i = 0; $i < @parts; $i++) {
282              
283 323         310 my $id_part = $parts[$i];
284              
285 323 100       610 if ($current_type->has_relationship($id_part)) {
    100          
    50          
286              
287 50         86 my $rel = $current_type->relationship($id_part);
288 50 100       109 my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
289 50         64 push @rels, $rel->{name};
290 50         77 my $current_rel_alias = join '_', @rels, 'link';
291              
292             # join relationship table
293 50 100       102 unless ($parser_data->{joined}{$current_rel_alias}) {
294              
295 49         905 push @{$parser_data->{joins}}, sprintf "INNER JOIN %sentity_relationships AS %s ON %s.id = %s.%s_entity_id AND %s.relationship_id = %d",
296             $eav->schema->table_prefix,
297             $current_rel_alias,
298             $current_entity_alias,
299             $current_rel_alias,
300             $our_side,
301             $current_rel_alias,
302 49         35 $rel->{id};
303              
304 49         476 $parser_data->{joined}{$current_rel_alias} = 1;
305             }
306              
307             # endpart is the relationship itself
308 50 100       98 if ($i == $#parts) {
309              
310 46 100       74 if (defined $bind_value) {
311              
312             die "Cursor: query error: the entity given to '$identifier' is not an entity of type '$rel->{entity}'."
313             unless blessed $bind_value
314             && $bind_value->isa('DBIx::EAV::Entity')
315 44 50 33     1142 && $bind_value->is_type($rel->{entity});
      33        
316              
317 44 50       100 die "Cursor: query error: the '$rel->{entity}' instance given to '$identifier' is not in storage."
318             unless $bind_value->in_storage;
319             }
320              
321             # set replacement for WHERE, and change bind value to the entity id
322             # note: dont cache this result because bindvalue can change
323             return {
324 46 100       155 replacement => $current_rel_alias .'.'. $their_side.'_entity_id',
325             bind => $bind_value ? $bind_value->id : '',
326             is_relationship => 1
327             }
328             }
329             # step into the related type
330             else {
331              
332 4         10 $current_type = $eav->type($rel->{entity});
333             $current_entity_alias = $current_entity_alias eq 'me' ? $rel->{name}
334 4 100       10 : $current_entity_alias.'_'.$rel->{name};
335              
336 4 50       8 unless ($parser_data->{joined}{$current_entity_alias}) {
337              
338 4         3 push @{$parser_data->{joins}}, sprintf "INNER JOIN %sentities AS %s ON %s.id = %s.%s_entity_id",
  4         56  
339             $eav->schema->table_prefix,
340             $current_entity_alias,
341             $current_entity_alias,
342             $current_rel_alias,
343             $their_side;
344              
345 4         32 $parser_data->{joined}{$current_entity_alias} = 1;
346             }
347             }
348              
349             }
350             elsif ($current_type->has_static_attribute($id_part)) {
351              
352             # attribute allowed only at the and
353 227 50       1406 confess "Cursor: query error: invalid identifier '$identifier': attribute only allowed at the and of identifier."
354             if $i < $#parts;
355              
356 227         953 return $parser_data->{cache}->{$identifier} = {
357             replacement => $current_entity_alias.'.'.$id_part,
358             };
359              
360             }
361             elsif ($current_type->has_attribute($id_part)) {
362              
363             # attribute allowed only at the and
364 46 50       88 confess "Cursor: query error: invalid identifier '$identifier': attribute only allowed at the and of identifier."
365             if $i < $#parts;
366              
367 46         99 my $attr = $current_type->attribute($id_part);
368             my $join_alias = $current_entity_alias eq 'me' ? $attr->{name}
369 46 100       96 : $current_entity_alias.'_'.$attr->{name};
370              
371 46 100       90 unless ($parser_data->{joined}{$join_alias}) {
372 34         592 push @{$parser_data->{joins}}, sprintf "LEFT JOIN %svalue_%s AS %s ON (%s.entity_id = %s.id AND %s.attribute_id = %s)",
373             $eav->schema->table_prefix,
374             $attr->{data_type},
375             $join_alias,
376             $join_alias,
377             $current_entity_alias,
378             $join_alias,
379 34         37 $attr->{id};
380              
381 34         360 $parser_data->{joined}{$join_alias} = 1;
382             }
383              
384 46         202 return { replacement => $join_alias.'.value' }
385             }
386             else {
387 0         0 die sprintf "Cursor: query error: invalid identifier '%s': '%s' is not a valid attribute/relationship for '%s'\n",
388             $identifier,
389             $id_part,
390             $current_type->name;
391             }
392             }
393              
394             }
395              
396             sub as_query {
397 6     6 0 990 \[shift->_build_sql_query];
398             }
399              
400              
401             sub reset {
402 3     3 1 3 my $self = shift;
403 3         37 $self->_reset_sth;
404 3         319 $self;
405             }
406              
407              
408             sub first {
409 2     2 1 25 $_[0]->reset->next;
410             }
411              
412              
413             sub next {
414 195     195 1 7279 my $self = shift;
415 195         2766 $self->_sth->fetchrow_hashref;
416             }
417              
418              
419             sub all {
420 0     0 1   my $self = shift;
421 0           my @rows;
422              
423 0           $self->reset;
424              
425 0           while (my $row = $self->next) {
426 0           push @rows, $row;
427             }
428              
429 0           $self->reset;
430              
431 0 0         return wantarray ? @rows : \@rows;
432             }
433              
434              
435              
436             1;
437              
438              
439             __END__