File Coverage

blib/lib/DBIx/QuickORM/Connection/RowData.pm
Criterion Covered Total %
statement 143 170 84.1
branch 51 88 57.9
condition 36 82 43.9
subroutine 25 30 83.3
pod 0 13 0.0
total 255 383 66.5


line stmt bran cond sub pod time code
1             package DBIx::QuickORM::Connection::RowData;
2 24     24   179 use strict;
  24         71  
  24         1151  
3 24     24   140 use warnings;
  24         226  
  24         2361  
4              
5             our $VERSION = '0.000019';
6              
7 24     24   177 use Carp qw/confess croak carp/;
  24         76  
  24         1997  
8 24     24   187 use List::Util qw/first/;
  24         63  
  24         1996  
9 24     24   156 use Scalar::Util qw/reftype blessed/;
  24         73  
  24         1708  
10              
11 24     24   192 use constant STORED => 'stored';
  24         50  
  24         2676  
12 24     24   346 use constant PENDING => 'pending';
  24         64  
  24         5683  
13 24     24   148 use constant DESYNC => 'desync';
  24         56  
  24         4967  
14 24     24   162 use constant TRANSACTION => 'transaction';
  24         67  
  24         11732  
15 24     24   197 use constant ROW_DATA => 'row_data';
  24         62  
  24         1620  
16              
17 24     24   192 use Importer Importer => 'import';
  24         55  
  24         221  
18             our @EXPORT_OK = qw{
19             STORED
20             PENDING
21             DESYNC
22             TRANSACTION
23             ROW_DATA
24             };
25              
26 24         303 use DBIx::QuickORM::Util::HashBase qw{
27             +connection
28             +source
29             +stack
30             +invalid
31 24     24   2685 };
  24         66  
32              
33 17 100   17 0 80 sub valid { $_[0]->active(no_fatal => 1) ? 1 : 0 }
34 0 0 0 0 0 0 sub invalid { $_[0]->active(no_fatal => 1) ? 0 : ($_[0]->{+INVALID} //= 'Unknown') }
35              
36             sub invalidate {
37 0     0 0 0 my $self = shift;
38 0         0 my %params = @_;
39              
40 0         0 my $reason = $params{reason};
41 0 0       0 unless ($reason) {
42 0         0 my @caller = caller;
43 0         0 $reason = "unkown at $caller[1] line $caller[2]";
44             }
45              
46 0         0 $self->{+INVALID} = $reason;
47              
48 0         0 my @old_stack = @{$self->{+STACK}};
  0         0  
49              
50 0 0 0     0 my $active = $self->active(no_fatal => 1) // @old_stack ? $old_stack[-1] : undef;
51              
52 0 0       0 my $pending = $active ? $active->{+PENDING} : undef;
53              
54 0 0 0     0 carp "Row invalidated with pending data" if $pending && keys %$pending;
55              
56 0         0 $self->{+STACK} = [];
57             }
58              
59 476     476 0 2297 sub source { $_[0]->{+SOURCE}->() }
60 94     94 0 304 sub connection { $_[0]->{+CONNECTION}->() }
61              
62 0     0 0 0 sub stored_data { $_[0]->active->{+STORED} }
63 2     2 0 11 sub pending_data { $_[0]->active->{+PENDING} }
64 0     0 0 0 sub desync_data { $_[0]->active->{+DESYNC} }
65 0     0 0 0 sub transaction { $_[0]->active->{+TRANSACTION} }
66              
67             sub init {
68 111     111 0 307 my $self = shift;
69              
70 111 50       590 my $src = $self->{+SOURCE} or confess "'source' is required";
71 111 50       474 my $con = $self->{+CONNECTION} or confess "'connection' is required";
72              
73 111         293 my ($src_sub, $src_obj);
74 111 50 50     651 if ((reftype($src) // '') eq 'CODE') {
75 0         0 $src_sub = $src;
76 0         0 $src_obj = $src_sub->();
77             }
78             else {
79 111         250 $src_obj = $src;
80 111     496   639 $src_sub = sub { $src_obj };
  496         3780  
81             }
82              
83 111 50 33     1212 croak "'source' must be either a blessed object that consumes the role 'DBIx::QuickORM::Role::Source', or a coderef that returns such an object"
      33        
84             unless $src_obj && blessed($src_obj) && $src_obj->DOES('DBIx::QuickORM::Role::Source');
85              
86 111         2644 my ($con_sub, $con_obj);
87 111 50 50     620 if ((reftype($con) // '') eq 'CODE') {
88 0         0 $con_sub = $con;
89 0         0 $con_obj = $con_sub->();
90             }
91             else {
92 111         313 $con_obj = $con;
93 111     99   491 $con_sub = sub { $con_obj };
  99         537  
94             }
95              
96 111 50 33     1286 croak "'connection' must be either a blessed instance of 'DBIx::QuickORM::Connection', or a coderef that returns such an object"
      33        
97             unless $con_obj && blessed($con_obj) && $con_obj->isa('DBIx::QuickORM::Connection');
98              
99 111         324 $self->{+CONNECTION} = $con_sub;
100 111         271 $self->{+SOURCE} = $src_sub;
101 111   50     666 $self->{+STACK} //= [];
102             }
103              
104             sub active {
105 467     467 0 995 my $self = shift;
106 467         1240 my %params = @_;
107              
108 467         807 my $connection;
109 467         1101 my $stack = $self->{+STACK};
110 467         24084 while (@$stack) {
111 465 100       1884 my $txn = $stack->[0]->{+TRANSACTION} or last; # No txn, bottom state
112 53   100     196 my $res = $txn->result // last; # Undef means still open
113 11         25 my $done = shift @$stack;
114              
115 11 100       45 next unless $res; # Rolled back
116              
117 7 50       24 if (@$stack) {
118 0         0 $self->_merge_state($done);
119             }
120             else {
121 7   33     42 $connection //= $self->connection;
122 7 50   4   40 $done->{+TRANSACTION} = first { !defined($_->result) && $_->id < $txn->id } reverse @{$connection->transactions};
  4         30  
  7         66  
123 7         57 push @$stack => $done;
124             }
125             }
126              
127 467 100       8369 return $stack->[0] if @$stack;
128              
129 13   100     47 $self->{+INVALID} //= "Likely inserted during a transaction that was rolled back";
130              
131 13 100       81 return if $params{no_fatal};
132 2         1318 confess "This row is invalid (Reason: $self->{+INVALID})";
133             }
134              
135             sub change_state {
136 65     65 0 154 my $self = shift;
137 65         204 my ($state) = @_;
138              
139 65 100       313 my $active = $self->active(no_fatal => 1) or return $self->_up_state($state);
140              
141 64         185 my $row_txn = $self->active->{+TRANSACTION};
142 64         189 my $state_txn = $state->{+TRANSACTION};
143              
144 64 100       204 my $state_res = $state_txn ? $state_txn->result : undef;
145 64 100       180 my $row_res = $row_txn ? $row_txn->result : undef;
146              
147 64 50 33     262 croak "Refusing to merge down a rolled-back transaction" if defined($state_res) && !$state_res;
148              
149 64         135 my $merge = 0;
150 64   66     600 $merge ||= !($row_txn || $state_txn);
      66        
151 64   66     224 $merge ||= $state_txn == $row_txn;
152              
153 64 50       225 if ($merge) {
154             # If the transactions are the same, or if there are no txns for eather, just merge.
155 64         320 $self->_merge_state($state);
156             }
157             else {
158 0         0 $self->_up_state($state);
159             }
160              
161 64         249 return $self;
162             }
163              
164             sub _up_state {
165 1     1   3 my $self = shift;
166 1         3 my ($state) = @_;
167              
168 1         3 my $stack = $self->{+STACK};
169 1 50 33     4 croak "There is already a base state, and no txn was provided" if @$stack && !$state->{+TRANSACTION};
170 1         4 unshift @$stack => $state;
171 1         3 return $self;
172             }
173              
174             sub _merge_state {
175 64     64   135 my $self = shift;
176 64         181 my ($merge, $source, $connection) = @_;
177              
178 64         197 my $into = $self->active;
179              
180 64 100       3422 if (my $stored = $merge->{+STORED}) {
    50          
181 56 100       241 if (my $pending = $into->{+PENDING}) {
182 13         31 for my $field (keys %{$merge->{+STORED}}) {
  13         67  
183 37   66     184 $source //= $self->source;
184 37   66     164 $connection //= $self->connection;
185              
186             # No change
187 37 100       143 next if $self->compare_field($field, $into->{+STORED}, $stored, $source, $connection);
188              
189 16         67 $into->{+STORED}->{$field} = $stored->{$field};
190 16 100       116 $into->{+DESYNC}->{$field} = 1 if $pending->{$field};
191             }
192 13 50       72 $into->{+PENDING} = $pending if keys %$pending;
193             }
194             else {
195 43         100 delete $into->{+DESYNC};
196 43 50 50     132 $into->{+STORED} = $into->{+STORED} ? {%{$into->{+STORED} // {}}, %{$stored}} : $stored;
  43         260  
  43         318  
197             }
198             }
199             elsif (exists $merge->{+STORED}) {
200 8         36 delete $into->{+STORED};
201 8         39 delete $into->{+DESYNC};
202 8         16 delete $merge->{+DESYNC};
203             }
204              
205 64 100 66     437 delete $into->{+DESYNC} if exists $merge->{+DESYNC} && !$merge->{+DESYNC};
206              
207 64         199 my $desync = $merge->{+DESYNC};
208 64 50       307 if (my $pending = $merge->{+PENDING}) {
    100          
209 0 0       0 $into->{+PENDING} = $into->{+PENDING} ? {%{$self->{+PENDING}}, %$pending} : $pending;
  0         0  
210 0 0       0 $into->{+DESYNC} = $into->{+DESYNC} ? {%{$self->{+DESYNC}}, %$desync} : $desync if $desync;
  0 0       0  
211             }
212             elsif (exists $merge->{+PENDING}) {
213 15         62 delete $into->{+PENDING};
214             }
215              
216 64 50 66     307 delete $into->{+PENDING} if $into->{+PENDING} && !keys %{$into->{+PENDING}};
  1         7  
217 64 100       240 delete $into->{+DESYNC} unless $into->{+PENDING};
218              
219 64         216 return $self;
220             }
221              
222             sub compare_field {
223 37     37 0 74 my $self = shift;
224 37         108 my ($field, $ah, $bh, $source, $connection) = @_;
225              
226 37   33     102 $source //= $self->source;
227 37   33     88 $connection //= $self->connection;
228              
229 37         217 my $affinity = $source->field_affinity($field, $connection->dialect);
230 37         131 my $type = $source->field_type($field);
231              
232 37         330 my $ae = exists $ah->{$field};
233 37         74 my $be = exists $bh->{$field};
234 37 100 50     228 return 0 if ($ae xor $be); # One exists, one does not
235 32 0 33     85 return 1 if (!$ae) && (!$be); # Neither exists
236              
237 32         84 my $a = $ah->{$field};
238 32         63 my $b = $bh->{$field};
239              
240 32         66 my $ad = defined($a);
241 32         57 my $bd = defined($b);
242 32 50 25     143 return 0 if ($ad xor $bd); # One is defined, one is not
243 32 0 33     90 return 1 if (!$ad) && (!$bd); # Neither is defined
244              
245             # true if different, false if same
246 32 100       119 return !$type->qorm_compare($a, $b) if $type;
247              
248             # true if same, false if different
249 25         124 return DBIx::QuickORM::Affinity::compare_affinity_values($affinity, $a, $b);
250             }
251              
252             1;