File Coverage

blib/lib/DBIx/Class/ParseError/Parser.pm
Criterion Covered Total %
statement 97 114 85.0
branch 19 36 52.7
condition 3 17 17.6
subroutine 15 16 93.7
pod 3 4 75.0
total 137 187 73.2


line stmt bran cond sub pod time code
1             package DBIx::Class::ParseError::Parser;
2              
3 4     4   2796 use Moo::Role;
  4         13  
  4         35  
4 4     4   1672 use Carp 'croak';
  4         10  
  4         263  
5 4     4   1752 use DBIx::Class::ParseError::Error;
  4         15  
  4         150  
6 4     4   30 use Regexp::Common qw(list);
  4         9  
  4         27  
7              
8             requires 'type_regex';
9              
10             has _schema => (
11             is => 'ro', required => 1, init_arg => 'schema',
12             );
13              
14             has custom_errors => (
15             is => 'ro',
16             default => sub { {} },
17             );
18              
19             # Feels weird putting the BUILD method in a role, but this effectively acts as
20             # a base class. We can't use a method modifier because BUILD isn't in the
21             # inheritance hierarchy of the classes and I didn't think it was appropriate
22             # to change too much.
23             sub BUILD {
24 4     4 0 6388 my $self = shift;
25 4         20 my $custom_errors = $self->custom_errors;
26 4         42 foreach my $type ( keys %$custom_errors ) {
27 0 0       0 unless ( $type =~ /^custom_/ ) {
28 0         0 $custom_errors->{"custom_$type"} = delete $custom_errors->{$type};
29 0         0 $type = "custom_$type";
30             }
31 0 0       0 unless ('Regexp' eq ref $custom_errors->{$type} ) {
32 0   0     0 my $ref = ref $custom_errors->{$type} || 'string';
33 0         0 croak("Custom errors should point to Regexp references, not '$ref': $type");
34             }
35             }
36             }
37              
38             has _source_table_map => (
39             is => 'lazy', builder => '_build_source_table_map',
40             );
41              
42             sub _build_source_table_map {
43 4     4   42 my $self = shift;
44 4         19 my $schema = $self->_schema;
45             return {
46             map {
47 4         34 my $source = $schema->source($_);
  12         931  
48 12         492 ( $schema->class($_) => $source, $source->from => $source )
49             } $schema->sources
50             };
51             }
52              
53             sub parse_type {
54 17     17 1 51 my ( $self, $error ) = @_;
55 17         99 my $custom_errors = $self->custom_errors;
56 17         100 my $type_regex = $self->type_regex;
57              
58             # try to match custom errors first
59 17         6842 foreach ( sort keys %$custom_errors ) {
60 0 0       0 if ( my @data = $error =~ $custom_errors->{$_} ) {
61             return {
62             name => $_,
63 0 0       0 data => [ grep { defined && length } @data ],
  0         0  
64             };
65             }
66             }
67 17         118 foreach ( sort keys %$type_regex ) {
68 63 100       554 if ( my @data = $error =~ $type_regex->{$_} ) {
69             return {
70             name => $_,
71 17 50       213 data => [ grep { defined && length } @data ],
  17         215  
72             };
73             }
74             }
75 0         0 return { name => 'unknown' };
76             }
77              
78             sub _add_info_from_type {
79 17     17   46 my ($self, $error_info, $error_type) = @_;
80 17         47 my $table = $error_info->{'table'};
81 17     6   91 my $replace_dots = sub { $_[0] =~ s{\.}{_}; $_[0] };
  6         26  
  6         35  
82 17     17   67 my $remove_table = sub { $_[0] =~ s{^$table\.}{}i; $_[0] };
  17         107  
  17         88  
83 17         363 my $source = $self->_source_table_map->{$table};
84             my $action_type_map = {
85             unique_key => sub {
86 6     6   44 my $unique_keys = { $source->unique_constraints };
87             my $unique_data = [
88 6         70 map { $replace_dots->($_) } @{ $error_type->{'data'} }
  6         22  
  6         30  
89             ];
90 6 100       30 if ( my $unique_cols = $unique_keys->{ $unique_data->[0] } ) {
91             $error_info->{'columns'} = [
92 2         8 map { $remove_table->($_) } @$unique_cols
  2         8  
93             ];
94             }
95             else {
96 4         12 $error_info->{'type'} = 'primary_key';
97             $error_info->{'columns'} = [
98 4         7 map { $remove_table->($_) } @{ $unique_keys->{'primary'} }
  4         12  
  4         13  
99             ];
100             }
101             },
102             primary_key => sub {
103             $error_info->{'columns'} = [
104 0     0   0 map { $remove_table->($_) } $source->primary_columns
  0         0  
105             ];
106             },
107             default => sub {
108 11 50   11   24 if ( @{ $error_type->{'data'} } ) {
  11         46  
109             $error_info->{'columns'} = [
110 11         23 map { $remove_table->($_) } @{ $error_type->{'data'} }
  11         83  
  11         31  
111             ];
112             }
113             },
114 17         322 };
115             ( $action_type_map->{ $error_type->{'name'} }
116 17   66     138 || $action_type_map->{'default'} )->();
117 17         374 return $error_info;
118             }
119              
120             sub _build_column_data {
121 12     12   40 my ($self, $column_keys, $column_values) = @_;
122 12         50 $column_keys =~ s{\s*=\s*\?}{}g;
123 12         88 $column_keys = [split(/\,\s+/, $column_keys)];
124 12 100       44 if ($column_values) {
125 11         48 $column_values =~ s{\'}{}g;
126             $column_values = [
127 11         58 map { (split(/=/))[1] }
  30         114  
128             split(/\,\s+/, $column_values)
129             ];
130             return {
131             map {
132 11         36 my $value = shift(@$column_values);
  28         55  
133 28 100       160 $_ => ($value =~ m/undef/ ? undef : $value)
134             } @$column_keys
135             };
136             }
137             else {
138 1         3 return { map { $_ => undef } @$column_keys };
  3         11  
139             }
140             }
141              
142             sub parse_general_info {
143 17     17 1 64 my ($self, $error, $error_type) = @_;
144              
145 17         93 my $insert_re = qr{
146             INSERT\s+INTO\s+
147             (\w+)\s+
148             \( \s* ($RE{list}{-pat => '\w+'}|\w+)\s* \)\s+
149             VALUES\s+
150             \( \s* (?:$RE{list}{-pat => '\?'}|\?)\s* \)\s*
151             (?:RETURNING\s+id)? # optional ID return from PostgreSQL
152             \s*\"
153             \s*\w*\s*\w*:?\s*
154             ($RE{list}{-pat => '\d=\'?[\w\s]+\'?'})?
155             }ix;
156              
157 17         6007 my $update_re = qr{
158             UPDATE\s+
159             (\w+)\s+
160             SET\s+
161             ($RE{list}{-pat => '\w+\s*\=\s*\?'}|\w+\s*\=\s*\?)\s*
162             (?:WHERE)?.*\"
163             \s*\w*\s*\w*:?\s*
164             ($RE{list}{-pat => '\d=\'?[\w\s]+\'?'})?
165             }ix;
166              
167 17         4084 my $missing_column_re = qr{
168             (store_column|get_column)\(\)\:\s+
169             no\s+such\s+column\s+['"](\w+)['"]\s+
170             on\s+($RE{list}{-pat => '\w+'}{-sep => '::'})
171             }ix;
172              
173 17         2843 my $source_table_map = $self->_source_table_map;
174              
175 17         453 my $error_info;
176             my $error_matched;
177 17 100       90 if ( $error =~ $insert_re ) {
    100          
    50          
    0          
178 9         295 my ($table, $column_keys, $column_values) = ($1, $2, $3);
179 9         49 $error_info = {
180             operation => 'insert',
181             table => $table,
182             column_data => $self->_build_column_data(
183             $column_keys, $column_values
184             ),
185             };
186 9         35 $error_matched = 1;
187             }
188             elsif ( $error =~ $update_re ) {
189 3         99 my ($table, $column_keys, $column_values) = ($1, $2, $3);
190 3         16 $error_info = {
191             operation => 'update',
192             table => $table,
193             column_data => $self->_build_column_data(
194             $column_keys, $column_values
195             ),
196             };
197 3         9 $error_matched = 1;
198             }
199             elsif ( $error =~ $missing_column_re ) {
200 5         167 my ($op_key, $column, $source_name) = ($1, $2, $3);
201 5         51 my $op_mapping = {
202             'store_column' => 'insert',
203             'get_column' => 'update',
204             };
205 5         19 my $source = $source_table_map->{ $source_name };
206             $error_info = {
207 5 50       60 operation => $op_mapping->{ lc $op_key },
208             $source ? ( table => $source->name ) : (),
209             columns => [$column],
210             };
211 5         20 $error_matched = 1;
212             }
213             elsif ( $error_type->{'name'} eq 'missing_table' ) {
214 0         0 my $table_name = $error_type->{'data'}[0];
215             $error_info = {
216             table => $table_name,
217             operation => q{},
218             columns => [],
219             column_data => {},
220 0         0 source_name => $source_table_map->{ $table_name }->source_name,
221             };
222 0         0 $error_matched = 1;
223             }
224              
225 17 50 50     125 if (my $source = $source_table_map->{ $error_info->{'table'} || '' }) {
226 17         89 $error_info->{'source_name'} = $source->source_name;
227             }
228              
229 17         41 my $type = $error_type->{name};
230              
231             # some databases may support more different error types. Those should be
232             # prefixed with "custom_" (such as "custom_unknown_function" or
233             # something). This allows different databases to present different error
234             # types.
235             #
236             # However, these errors come in many sizes and shapes. We can't
237             # deterministically say what the columns, operation or *anything* really
238             # is, so we just punt and hand it back to the developer.
239 17 50       64 if ( $type =~ /^custom_/ ) {
240             return {
241             column_data => ( $error_info->{column_data} || {} ),
242             columns => ( $error_info->{columns} || [] ),
243             operation => ( $error_info->{operation} || '' ),
244             source_name => ( $error_info->{source_name} || '' ),
245 0   0     0 table => ( $error_info->{table} || '' ),
      0        
      0        
      0        
      0        
246             type => $type,
247             };
248             }
249              
250 17 50       55 unless ($error_matched) {
251 0         0 die 'Parsing error string failed';
252             }
253              
254 17         67 return $self->_add_info_from_type($error_info, $error_type);
255             }
256              
257             sub process {
258 17     17 1 317 my ($self, $error) = @_;
259 17         94 my $error_type = $self->parse_type($error);
260             my $err_info = {
261             type => $error_type->{'name'},
262 17         64 %{ $self->parse_general_info($error, $error_type) },
  17         103  
263             };
264 17         100 return DBIx::Class::ParseError::Error->new(
265             message => "$error",
266             %$err_info,
267             );
268             }
269              
270             1;
271              
272             __END__