File Coverage

lib/DBIx/EAV/Cursor.pm
Criterion Covered Total %
statement 168 178 94.3
branch 77 94 81.9
condition 4 9 44.4
subroutine 12 14 85.7
pod 4 5 80.0
total 265 300 88.3


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