File Coverage

lib/DBIx/EAV/Cursor.pm
Criterion Covered Total %
statement 15 177 8.4
branch 0 94 0.0
condition 0 9 0.0
subroutine 5 14 35.7
pod 4 5 80.0
total 24 299 8.0


line stmt bran cond sub pod time code
1             package DBIx::EAV::Cursor;
2              
3 10     10   32 use Moo;
  10         10  
  10         38  
4 10     10   1715 use Carp qw/ croak confess /;
  10         16  
  10         402  
5 10     10   34 use Data::Dumper;
  10         15  
  10         333  
6 10     10   6506 use SQL::Abstract;
  10         74331  
  10         429  
7 10     10   53 use Scalar::Util qw/ blessed /;
  10         10  
  10         16981  
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     my $self = shift;
22 0 0         croak "Query already executed!" if defined $self->_sth;
23             }
24              
25              
26             sub _build__sth {
27 0     0     my $self = shift;
28 0           my ($sql_query, $bind) = $self->_build_sql_query();
29 0           my ($rv, $sth) = $self->eav->table('entities')->_do($sql_query, $bind);
30 0           $sth;
31             }
32              
33              
34             sub _build_sql_query {
35 0     0     my $self = shift;
36              
37 0           my $opts = $self->options;
38 0           my $eav = $self->eav;
39 0           my $type = $self->type;
40 0           my $entities_table = $eav->table('entities');
41 0           my ($order_by, $group_by, $having, %parser_data, %replacements);
42              
43             # selected field
44 0           my @select_fields = $opts->{select} ? @{$opts->{select}}
45 0 0         : @{$entities_table->columns};
  0            
46              
47             # distinct (before normalizing @select_fields)
48 0 0         if ($opts->{distinct}) {
49              
50             # if has group_by, warn and ignore distinct
51 0 0         if ($opts->{group_by}) {
52              
53             }
54             else {
55             # exclude id from group by to make the distinct effect
56 0   0       $opts->{group_by} = [grep { !ref && $_ !~ /^(me\.|)id$/ } @select_fields];
  0            
57             }
58             }
59              
60             # normalize select fields
61 0           for (my $i = 0; $i < @select_fields; $i++) {
62              
63             # literal, dont touch
64 0 0         next if ref $select_fields[$i] eq 'SCALAR';
65              
66 0           my $ident = $select_fields[$i];
67 0           my ($fn, $as);
68              
69             # sql function
70 0 0         if (ref $ident) {
71              
72 0           $as = delete $ident->{'-as'};
73 0           ($fn, $ident) = each %$ident;
74              
75 0 0         unless ($as) {
76 0           $as = lc($fn.'_'.$ident);
77 0           $as =~ s/\./_/g;
78             }
79              
80 0           $parser_data{aliases}{$as} = 1;
81             }
82              
83 0           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 0 0         : $info->{replacement};
87             }
88              
89             # add type criteria unless we have a subselect ('from' option)
90 0 0         my $type_criteria = $opts->{from} ? [] : [ entity_type_id => $type->id ];
91              
92 0 0         if ($opts->{subtype_depth}) {
93             push @$type_criteria, [ '_parent_type_'.$_, $type->id ]
94 0           for 1 .. $opts->{subtype_depth};
95             }
96              
97             # parse WHERE
98 0           my ($where, @bind) = $sql->where({ -and => [ $type_criteria, $self->query] });
99 0           my $i = 0;
100              
101 0           my $where_re = qr/ ([\w._]+) (?:=|!=|<>|>|<|>=|<=|IN|IS NULL|LIKE|NOT LIKE) \?/;
102 0           while ($where =~ /$where_re/g) {
103              
104 0           my $ident = $1;
105 0           my $info = $self->_parse_clause_identifier($ident, \%parser_data, $bind[$i]);
106              
107             $bind[$i] = $info->{bind}
108 0 0         if exists $info->{bind};
109              
110 0           $replacements{$ident} = $info->{replacement};
111              
112 0           $i++;
113             }
114              
115             # replace identifiers in WHERE
116 0           while (my ($string, $replacement) = each %replacements) {
117 0           $where =~ s/\b$string\b/$replacement/g;
118             }
119              
120             # parse ORDER BY
121 0 0         if (defined $opts->{order_by}) {
122              
123 0           %replacements = ();
124 0           $order_by = $sql->where(undef, $opts->{order_by});
125              
126 0           while ($order_by =~ / ([\w._]+)(?: ASC| DESC|,|$)/g) {
127              
128 0           my $ident = $1;
129 0           my $info = $self->_parse_clause_identifier($ident, \%parser_data);
130              
131             die "Cursor: query error: can't order by relationship! ($ident)'"
132 0 0         if $info->{is_relationship};
133              
134 0           $replacements{$ident} = $info->{replacement};
135             }
136              
137             # replace identifiers
138 0           while (my ($string, $replacement) = each %replacements) {
139 0           $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 0 0         if (defined $opts->{group_by}) {
156              
157 0           my @fields;
158              
159 0 0         foreach my $ident (ref $opts->{group_by} eq 'ARRAY' ? @{$opts->{group_by}} : $opts->{group_by}) {
  0            
160              
161 0           my $info = $self->_parse_clause_identifier($ident, \%parser_data);
162              
163             die "Cursor: query error: can't group by a relationship! ($ident)'"
164 0 0         if $info->{is_relationship};
165              
166 0           push @fields, $info->{replacement};
167             }
168              
169 0           $group_by .= 'GROUP BY '. join(', ', @fields);
170             }
171              
172             # parse HAVING
173 0 0         if (defined $opts->{having}) {
174              
175 0           my @having_bind;
176 0           ($having, @having_bind) = $sql->where($opts->{having});
177              
178 0           push @bind, @having_bind;
179 0           $having =~ s/^\s*WHERE/HAVING/;
180              
181 0           %replacements = ();
182 0           while ($having =~ /$where_re/g) {
183              
184 0           my $ident = $1;
185 0           my $info = $self->_parse_clause_identifier($ident, \%parser_data);
186              
187 0           $replacements{$ident} = $info->{replacement};
188             }
189              
190             # replace identifiers
191 0           while (my ($string, $replacement) = each %replacements) {
192 0           $having =~ s/\b$string\b/$replacement/g;
193             }
194             }
195              
196             # build sql statement
197              
198             # SELECT ... FROM
199              
200             # from subselect
201 0           my $from = $entities_table->name;
202 0 0         if (my $subquery = $opts->{from}) {
203              
204 0           my ($sub_select, $sub_bind) = @$$subquery;
205 0           $from = "($sub_select)";
206 0           push @bind, @$sub_bind;
207             }
208              
209 0           my $sql_query = $sql->select("$from AS me", \@select_fields);
210              
211             # JOINs
212 0 0         if (my $depth = $opts->{subtype_depth}) {
213              
214 0           my $hierarchy_table = $eav->table("type_hierarchy")->name;
215 0           $sql_query .= " LEFT JOIN $hierarchy_table AS _parent_type_1 ON (_parent_type_1.child_type_id = me.entity_type_id)";
216 0           my $i = 2;
217 0           while ($depth > 1) {
218 0           $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 0           $depth--;
221 0           $i++;
222             }
223             }
224              
225 0 0         $sql_query .= " $_" for @{$parser_data{joins} || []};
  0            
226              
227             # WHERE, GROUP BY, HAVING, ORDER BY
228 0           $sql_query .= " $where";
229 0 0         $sql_query .= " $group_by" if defined $group_by;
230 0 0         $sql_query .= " $having" if defined $having;
231 0 0         $sql_query .= " $order_by" if defined $order_by;
232              
233             # LIMIT / OFFSET
234 0 0         if ($opts->{limit}) {
235 0 0         die "invalid limit" unless $opts->{limit} =~ /^\d+$/;
236 0           $sql_query .= " LIMIT $opts->{limit}";
237              
238 0 0         if (defined $opts->{offset}) {
239 0 0         die "invalid offset" unless $opts->{offset} =~ /^\d+$/;
240 0           $sql_query .= " OFFSET $opts->{offset}";
241             }
242             }
243              
244             # return query and bind values
245 0           ($sql_query, \@bind);
246             }
247              
248             sub _parse_clause_identifier {
249 0     0     my ($self, $identifier, $parser_data, $bind_value) = @_;
250              
251             # cached
252             return $parser_data->{cache}->{$identifier}
253 0 0         if exists $parser_data->{cache}->{$identifier};
254              
255 0           my $type = $self->type;
256 0           my $eav = $self->eav;
257              
258             # special case: parent_type
259 0 0         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 0 0         if exists $parser_data->{aliases}{$identifier};
265              
266             # remove me.
267 0           $identifier =~ s/^me\.//;
268              
269             # parse possibly deep related identifier
270             # valid formats:
271             # -
272             # -
273             # - +.
274              
275 0           my @parts = split /\./, $identifier;
276 0           my @joins;
277 0           my $current_type = $type;
278 0           my $current_entity_alias = 'me';
279 0           my @rels;
280              
281 0           for (my $i = 0; $i < @parts; $i++) {
282              
283 0           my $id_part = $parts[$i];
284              
285 0 0         if ($current_type->has_relationship($id_part)) {
    0          
    0          
286              
287 0           my $rel = $current_type->relationship($id_part);
288 0 0         my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
289 0           push @rels, $rel->{name};
290 0           my $current_rel_alias = join '_', @rels, 'link';
291              
292             # join relationship table
293 0 0         unless ($parser_data->{joined}{$current_rel_alias}) {
294              
295 0           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 0           $rel->{id};
303              
304 0           $parser_data->{joined}{$current_rel_alias} = 1;
305             }
306              
307             # endpart is the relationship itself
308 0 0         if ($i == $#parts) {
309              
310 0 0         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 0 0 0       && $bind_value->is_type($rel->{entity});
      0        
316              
317 0 0         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 0 0         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 0           $current_type = $eav->type($rel->{entity});
333             $current_entity_alias = $current_entity_alias eq 'me' ? $rel->{name}
334 0 0         : $current_entity_alias.'_'.$rel->{name};
335              
336 0 0         unless ($parser_data->{joined}{$current_entity_alias}) {
337              
338 0           push @{$parser_data->{joins}}, sprintf "INNER JOIN %sentities AS %s ON %s.id = %s.%s_entity_id",
  0            
339             $eav->schema->table_prefix,
340             $current_entity_alias,
341             $current_entity_alias,
342             $current_rel_alias,
343             $their_side;
344              
345 0           $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 0 0         confess "Cursor: query error: invalid identifier '$identifier': attribute only allowed at the and of identifier."
354             if $i < $#parts;
355              
356 0           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 0 0         confess "Cursor: query error: invalid identifier '$identifier': attribute only allowed at the and of identifier."
365             if $i < $#parts;
366              
367 0           my $attr = $current_type->attribute($id_part);
368             my $join_alias = $current_entity_alias eq 'me' ? $attr->{name}
369 0 0         : $current_entity_alias.'_'.$attr->{name};
370              
371 0 0         unless ($parser_data->{joined}{$join_alias}) {
372 0           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 0           $attr->{id};
380              
381 0           $parser_data->{joined}{$join_alias} = 1;
382             }
383              
384 0           return { replacement => $join_alias.'.value' }
385             }
386             else {
387 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 0     0 0   \[shift->_build_sql_query];
398             }
399              
400              
401             sub reset {
402 0     0 1   my $self = shift;
403 0           $self->_reset_sth;
404 0           $self;
405             }
406              
407              
408             sub first {
409 0     0 1   $_[0]->reset->next;
410             }
411              
412              
413             sub next {
414 0     0 1   my $self = shift;
415 0           $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__