File Coverage

blib/lib/RapidApp/DBIC/Component/VirtualColumnsExt.pm
Criterion Covered Total %
statement 99 107 92.5
branch 30 46 65.2
condition 11 14 78.5
subroutine 19 21 90.4
pod 6 14 42.8
total 165 202 81.6


line stmt bran cond sub pod time code
1             package RapidApp::DBIC::Component::VirtualColumnsExt;
2 5     5   2869 use strict;
  5         12  
  5         131  
3 5     5   24 use warnings;
  5         11  
  5         320  
4              
5             #use base 'DBIx::Class';
6             # this is for Attribute::Handlers:
7             require base; base->import('DBIx::Class');
8              
9             ### FIXME - not sure why the above happens at runtime, but not digging in
10             ### due to severe lack of time
11             ###
12             ### Still we need to apply the markers otherwise things will be *LOUD*
13             for my $m (qw( has_column columns )) {
14             attributes->import(
15             __PACKAGE__,
16             __PACKAGE__->can($m),
17             'DBIC_method_is_bypassable_resultsource_proxy',
18             );
19             }
20              
21 5     5   26 use RapidApp::Util qw(:all);
  5         11  
  5         8404  
22              
23 0     0   0 sub _skip_namespace_frames { qr/^RapidApp::DBIC::Component/ }
24              
25             # Load the vanilla/original DBIx::Class::VirtualColumns component:
26             __PACKAGE__->load_components('+RapidApp::DBIC::Component::VirtualColumns');
27              
28             __PACKAGE__->mk_classdata( '_virtual_columns_order' );
29              
30             sub init_vcols_class_data {
31 1833     1833 0 2690 my $self = shift;
32            
33 1833 100       32078 $self->_virtual_columns( {} )
34             unless defined $self->_virtual_columns();
35            
36 1833 100       73471 $self->_virtual_columns_order( [] )
37             unless defined $self->_virtual_columns_order();
38             }
39              
40             # extend add_virtual_columns to also track column order
41             sub add_virtual_columns {
42 58     58 1 616697 my $self = shift;
43 58         130 my @columns = @_;
44            
45 58         296 $self->init_vcols_class_data;
46            
47 58         7440 foreach my $column (@columns) {
48             next if (
49             ref $column or
50             $self->has_column($column) or
51 8 50 66     94 exists $self->_virtual_columns->{$column} #<-- redundant since we override 'has_column'
      66        
52             );
53            
54 4         485 push @{$self->_virtual_columns_order}, $column;
  4         66  
55             }
56            
57 58         199 return $self->next::method(@_);
58             }
59              
60             sub virtual_columns {
61 513     513 0 35438 my $self = shift;
62 513         1516 $self->init_vcols_class_data;
63 513         9027 return @{$self->_virtual_columns_order};
  513         8302  
64             }
65              
66             # Take-over has_column to include virtual columns
67             sub has_column {
68 749     749 0 1230 my $self = shift;
69 749         1060 my $column = shift;
70 749         1829 $self->init_vcols_class_data;
71 749 100 100     26503 return ($self->_virtual_columns->{$column} ||
72             $self->next::method($column)) ? 1:0;
73             }
74              
75             # Take-over columns to include virtual columns:
76             sub columns {
77 513     513 0 5168 my $self = shift;
78 513         1742 $self->init_vcols_class_data;
79 513         11437 return ($self->next::method(@_),$self->virtual_columns);
80             }
81              
82              
83             sub get_column {
84 294     294 1 400258 my ($self, $column) = @_;
85              
86             return $self->next::method($column) unless (
87             defined $self->_virtual_columns &&
88 294 100 66     5210 exists $self->_virtual_columns->{$column}
89             );
90            
91 2         196 $self->init_virtual_column_value($column);
92            
93 2         8 return $self->next::method($column);
94             }
95              
96             # TODO/FIXME:
97             # here we are loading/appending all defined virtual columns during the call
98             # to get_columns(), which is slow/expensive. What we should be doing is
99             # hooking into 'inflate_result' to *preload* the values into the row object
100             # ('_column_data' attr) in the same location as ordinary columns which should
101             # then allow the native get_columns() to work as-is...
102             sub get_columns {
103 32     32 1 198495 my $self = shift;
104            
105 32 100       169 return $self->next::method(@_) unless $self->in_storage;
106 16         78 my %data = $self->next::method(@_);
107            
108 16 50       284 if (defined $self->_virtual_columns) {
109 16         587 foreach my $column (keys %{$self->_virtual_columns}) {
  16         277  
110 16         564 my $value = undef;
111 16 100       87 $data{$column} = $value
112             if($self->init_virtual_column_value($column,\$value));
113             }
114             }
115 16         146 return %data;
116             }
117              
118              
119             # ---
120             # Get the safe "select" string for any column. For virtual columns,
121             # this will be a special SQLT-style reference, for normal columns
122             # just the column name. TODO/FIXME: this code is not complete as
123             # it is currently hard-coded for 'me' -- which means it will break
124             # if 'me' isn't the alias, or if joined. This is just partial/demo
125             # code for now... Need to actually learn the right DBIC way to do
126             # this. This is a BUG. This code has been added for 2 reasons:
127             # 1. As a reminder to fix this
128             # 2. As a stop-gap for RapidApp::Module::DbicCombo (sort), which is also
129             # in a tmp/in-flux state. Otherwise, this code would just be
130             # there, but it is here as a reminder. No other code should be
131             # calling this **private** method which WILL change when this
132             # is actually addressed for real.
133             sub _virtual_column_select {
134 0     0   0 my ($self, $column) = @_;
135 0 0       0 if ($self->has_virtual_column($column)) {
    0          
136 0         0 my $rel = 'me';
137 0         0 return $self->_get_virtual_column_select_statement($column, $rel);
138             }
139             elsif($self->has_column($column)) {
140 0         0 return "me.$column";
141             }
142 0         0 die "_virtual_column_select(): Unknown column '$column'";
143             }
144             # ---
145              
146             sub _get_virtual_column_select_statement {
147 9     9   30 my ($self, $column, $rel) = @_;
148 9         61 my $info = $self->column_info($column);
149             my $sql = $info->{sql}
150 9 50       939 or die "Missing virtual column 'sql' attr in info";
151             # also see RapidApp::TableSpec::Role::DBIC
152 9 50       31 $sql = $info->{sql}->($self, $column) if ref $sql eq 'CODE';
153 9         24 $sql =~ s/self\./${rel}\./g;
154 9         37 $sql =~ s/\`self\`\./\`${rel}\`\./g; #<-- also support backtic quoted form (quote_sep)
155 9         67 return \"($sql)";
156             }
157              
158             sub init_virtual_column_value {
159 18     18 0 46 my ($self, $column,$valref) = @_;
160 18 100       67 return if (exists $self->{_virtual_values}{$column});
161 9         25 my $rel = 'me';
162 9         51 my $sql = $self->_get_virtual_column_select_statement($column, $rel);
163 9         43 my $Source = $self->result_source;
164 9         87 my $cond = { map { $rel . '.' . $_ => $self->get_column($_) } $Source->primary_columns };
  9         103  
165 9         225 my $attr = {
166             select => [{ '' => $sql, -as => $column }],
167             as => [$column],
168             result_class => 'DBIx::Class::ResultClass::HashRefInflator'
169             };
170 9         33 my $info = $self->column_info($column);
171 9 50       767 $attr->{join} = $info->{join} if (exists $info->{join});
172            
173 9 50       37 my $row = $Source->resultset->search_rs($cond,$attr)->first or return undef;
174            
175             # optionally update a supplied reference, passed by argument:
176 9 50       27560 $$valref = $row->{$column} if (ref($valref) eq 'SCALAR');
177            
178 9         1343 local $self->{_virtual_columns_no_prepare_set} = 1;
179 9         66 return $self->store_column($column,$row->{$column});
180             }
181              
182              
183             # Prepares any set_functions, if applicable, for the supplied col/vals
184             # ('set_functions' are custom coderefs optionally defined in the attributes
185             # of a virtaul column. Similar concept to the 'sql' attribute but for update/insert
186             # instead of select. Also, 'set_function' is a Perl coderef which calls
187             # DBIC methods while 'sql' is raw SQL code passed off to the DB)
188             sub prepare_set {
189 228     228 0 391 my $self = shift;
190 228 50       749 my %opt = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref
  0         0  
191            
192             # Check special flag to abort preparing set functions
193 228 100       576 return if ($self->{_virtual_columns_no_prepare_set});
194            
195 213 50       4029 return unless (defined $self->_virtual_columns);
196 213   100     9386 $self->{_virtual_columns_pending_set_function} ||= {};
197 213         536 foreach my $column (keys %opt) {
198 188 100       3229 next unless (exists $self->_virtual_columns->{$column});
199 3 50   3   131 my $coderef = try{$self->column_info($column)->{set_function}} or next;
  3         89  
200             $self->{_virtual_columns_pending_set_function}{$column} = {
201             coderef => $coderef,
202 3         340 value => $opt{$column}
203             };
204             }
205             }
206              
207             sub execute_pending_set_functions {
208 27     27 0 59 my $self = shift;
209 27 50       111 my $pend = $self->{_virtual_columns_pending_set_function} or return;
210 27         102 foreach my $column (keys %$pend) {
211 3         9 my $h = delete $pend->{$column}; #<-- fetch and clear
212 3         22 $h->{coderef}->($self,$h->{value});
213             }
214             }
215              
216             sub store_column {
217 121     121 1 35168 my ($self, $column, $value) = @_;
218 121         473 $self->prepare_set($column,$value);
219 121         4230 return $self->next::method($column, $value);
220             }
221              
222             sub set_column {
223 80     80 1 111203 my ($self, $column, $value) = @_;
224 80         368 $self->prepare_set($column,$value);
225 80         3068 return $self->next::method($column, $value);
226             }
227              
228             sub update {
229 11     11 1 113 my $self = shift;
230 11         42 $self->prepare_set(@_);
231            
232             # Disable preparing any more set functions. We need to do this
233             # because update() will call store_column() on every column (which
234             # in turn calls prepare_set). We only want to prepare_set for
235             # virtual columns which are actually being changed, and that has
236             # already happened by this point.
237 11         43 local $self->{_virtual_columns_no_prepare_set} = 1;
238            
239             # Do regular update
240 11         51 $self->next::method(@_);
241            
242 11         74 $self->execute_pending_set_functions;
243            
244             # NEW: if any of the set functions have left the row dirty, update again:
245             # (principal of least astonishment)
246 11         170 my %dirty = $self->get_dirty_columns;
247 11 50       92 $self->next::method if(scalar(keys %dirty) > 0);
248            
249 11         35 return $self;
250             }
251              
252             sub insert {
253 16     16 0 53403 my $self = shift;
254 16         56 $self->prepare_set(@_);
255            
256             # Do regular insert
257 16         64 $self->next::method(@_);
258            
259 16         38366 $self->execute_pending_set_functions;
260 16         91 return $self;
261             }
262              
263              
264             1;