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   3768 use strict;
  5         15  
  5         155  
3 5     5   29 use warnings;
  5         12  
  5         381  
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   34 use RapidApp::Util qw(:all);
  5         11  
  5         9764  
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 1320     1320 0 2261 my $self = shift;
32            
33 1320 100       26187 $self->_virtual_columns( {} )
34             unless defined $self->_virtual_columns();
35            
36 1320 100       55512 $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 692100 my $self = shift;
43 58         158 my @columns = @_;
44            
45 58         311 $self->init_vcols_class_data;
46            
47 58         8615 foreach my $column (@columns) {
48             next if (
49             ref $column or
50             $self->has_column($column) or
51 8 50 66     121 exists $self->_virtual_columns->{$column} #<-- redundant since we override 'has_column'
      66        
52             );
53            
54 4         508 push @{$self->_virtual_columns_order}, $column;
  4         104  
55             }
56            
57 58         226 return $self->next::method(@_);
58             }
59              
60             sub virtual_columns {
61 380     380 0 29817 my $self = shift;
62 380         1033 $self->init_vcols_class_data;
63 380         5859 return @{$self->_virtual_columns_order};
  380         6364  
64             }
65              
66             # Take-over has_column to include virtual columns
67             sub has_column {
68 502     502 0 978 my $self = shift;
69 502         849 my $column = shift;
70 502         1450 $self->init_vcols_class_data;
71 502 100 100     18234 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 380     380 0 5615 my $self = shift;
78 380         1338 $self->init_vcols_class_data;
79 380         7387 return ($self->next::method(@_),$self->virtual_columns);
80             }
81              
82              
83             sub get_column {
84 298     298 1 498116 my ($self, $column) = @_;
85              
86             return $self->next::method($column) unless (
87             defined $self->_virtual_columns &&
88 298 100 66     7321 exists $self->_virtual_columns->{$column}
89             );
90            
91 2         282 $self->init_virtual_column_value($column);
92            
93 2         9 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 244984 my $self = shift;
104            
105 32 100       208 return $self->next::method(@_) unless $self->in_storage;
106 16         106 my %data = $self->next::method(@_);
107            
108 16 50       352 if (defined $self->_virtual_columns) {
109 16         750 foreach my $column (keys %{$self->_virtual_columns}) {
  16         326  
110 16         696 my $value = undef;
111 16 100       102 $data{$column} = $value
112             if($self->init_virtual_column_value($column,\$value));
113             }
114             }
115 16         188 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   33 my ($self, $column, $rel) = @_;
148 9         83 my $info = $self->column_info($column);
149             my $sql = $info->{sql}
150 9 50       1223 or die "Missing virtual column 'sql' attr in info";
151             # also see RapidApp::TableSpec::Role::DBIC
152 9 50       44 $sql = $info->{sql}->($self, $column) if ref $sql eq 'CODE';
153 9         32 $sql =~ s/self\./${rel}\./g;
154 9         29 $sql =~ s/\`self\`\./\`${rel}\`\./g; #<-- also support backtic quoted form (quote_sep)
155 9         47 return \"($sql)";
156             }
157              
158             sub init_virtual_column_value {
159 18     18 0 60 my ($self, $column,$valref) = @_;
160 18 100       95 return if (exists $self->{_virtual_values}{$column});
161 9         23 my $rel = 'me';
162 9         74 my $sql = $self->_get_virtual_column_select_statement($column, $rel);
163 9         47 my $Source = $self->result_source;
164 9         110 my $cond = { map { $rel . '.' . $_ => $self->get_column($_) } $Source->primary_columns };
  9         146  
165 9         292 my $attr = {
166             select => [{ '' => $sql, -as => $column }],
167             as => [$column],
168             result_class => 'DBIx::Class::ResultClass::HashRefInflator'
169             };
170 9         49 my $info = $self->column_info($column);
171 9 50       931 $attr->{join} = $info->{join} if (exists $info->{join});
172            
173 9 50       51 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       36617 $$valref = $row->{$column} if (ref($valref) eq 'SCALAR');
177            
178 9         1611 local $self->{_virtual_columns_no_prepare_set} = 1;
179 9         99 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 232     232 0 465 my $self = shift;
190 232 50       967 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 232 100       820 return if ($self->{_virtual_columns_no_prepare_set});
194            
195 217 50       4968 return unless (defined $self->_virtual_columns);
196 217   100     11317 $self->{_virtual_columns_pending_set_function} ||= {};
197 217         644 foreach my $column (keys %opt) {
198 192 100       4069 next unless (exists $self->_virtual_columns->{$column});
199 3 50   3   157 my $coderef = try{$self->column_info($column)->{set_function}} or next;
  3         97  
200             $self->{_virtual_columns_pending_set_function}{$column} = {
201             coderef => $coderef,
202 3         383 value => $opt{$column}
203             };
204             }
205             }
206              
207             sub execute_pending_set_functions {
208 27     27 0 679 my $self = shift;
209 27 50       136 my $pend = $self->{_virtual_columns_pending_set_function} or return;
210 27         141 foreach my $column (keys %$pend) {
211 3         8 my $h = delete $pend->{$column}; #<-- fetch and clear
212 3         17 $h->{coderef}->($self,$h->{value});
213             }
214             }
215              
216             sub store_column {
217 123     123 1 41143 my ($self, $column, $value) = @_;
218 123         505 $self->prepare_set($column,$value);
219 123         5069 return $self->next::method($column, $value);
220             }
221              
222             sub set_column {
223 82     82 1 55861 my ($self, $column, $value) = @_;
224 82         405 $self->prepare_set($column,$value);
225 82         4215 return $self->next::method($column, $value);
226             }
227              
228             sub update {
229 11     11 1 147 my $self = shift;
230 11         48 $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         47 local $self->{_virtual_columns_no_prepare_set} = 1;
238            
239             # Do regular update
240 11         70 $self->next::method(@_);
241            
242 11         94 $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         227 my %dirty = $self->get_dirty_columns;
247 11 50       120 $self->next::method if(scalar(keys %dirty) > 0);
248            
249 11         57 return $self;
250             }
251              
252             sub insert {
253 16     16 0 57687 my $self = shift;
254 16         76 $self->prepare_set(@_);
255            
256             # Do regular insert
257 16         77 $self->next::method(@_);
258            
259 16         49174 $self->execute_pending_set_functions;
260 16         112 return $self;
261             }
262              
263              
264             1;