File Coverage

blib/lib/Mojo/mysql/Database/Role/LoadDataInfile.pm
Criterion Covered Total %
statement 27 122 22.1
branch 7 70 10.0
condition 3 61 4.9
subroutine 6 20 30.0
pod 2 2 100.0
total 45 275 16.3


line stmt bran cond sub pod time code
1             package Mojo::mysql::Database::Role::LoadDataInfile;
2 17     17   7862190 use Mojo::Base -role;
  17         209  
  17         133  
3 17     17   8151 use Mojo::File 'tempfile';
  17         45  
  17         897  
4 17     17   110 use Mojo::Util ();
  17         41  
  17         2540  
5              
6             our $VERSION = '0.01';
7              
8             my $text_csv_package;
9             BEGIN {
10 17 50 33 17   165 if (not $ENV{MOJO_MYSQL_DATABASE_ROLE_LOAD_DATA_INFILE_NO_XS} and eval { require Text::CSV_XS; 1 }) {
  17         16135  
  17         200666  
11 17         990 Text::CSV_XS->import('csv');
12 17         4776 $text_csv_package = 'Text::CSV_XS';
13             } else {
14 0         0 require Text::CSV_PP;
15 0         0 Text::CSV_PP->import('csv');
16 0         0 $text_csv_package = 'Text::CSV_PP';
17             }
18             }
19              
20             sub import {
21 22     22   5089 my $class = shift;
22 22 100       129 if (grep { $_ eq '-no_apply' } @_) {
  17         59  
23 3 100       2841 return if @_ == 1;
24 2         41 Carp::croak 'no other options may be provided with -no_apply';
25             }
26              
27 19         66 my %options = @_;
28 19   100     116 my $database_class = delete $options{database_class} || 'Mojo::mysql::Database';
29              
30 19 100       87 Carp::croak 'unknown options provided to import: ' . Mojo::Util::dumper(\%options) if %options;
31              
32 17         270 require Role::Tiny;
33 17         134 Role::Tiny->apply_roles_to_package($database_class, 'Mojo::mysql::Database::Role::LoadDataInfile');
34             }
35              
36             sub load_data_infile {
37 0     0 1   my $self = shift;
38 0 0         my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
39              
40             my (
41 0           $low_priority,
42             $concurrent,
43             $replace,
44             $ignore,
45             $table,
46             $partition,
47             $character_set,
48             $tempfile_open_mode,
49             $set,
50             $rows,
51             $columns,
52             $headers) = _parse_options(@_);
53              
54 0           my $tempfile = _write_temp_file($tempfile_open_mode, $rows, $headers);
55              
56 0           my $query = _build_query($low_priority, $concurrent, $tempfile, $replace, $ignore, $table, $partition, $character_set, $columns, $set);
57              
58 0 0         if ($cb) {
59             my $cb_wrapper = sub {
60 17     17   161 no warnings 'void';
  17         52  
  17         27073  
61 0     0     $tempfile;
62 0           $cb->(@_);
63 0           };
64              
65 0           return $self->query($query, $cb_wrapper);
66             } else {
67 0           return $self->query($query);
68             }
69             }
70              
71             sub load_data_infile_p {
72 0     0 1   my $promise = Mojo::Promise->new;
73              
74 0 0   0     shift->load_data_infile(@_ => sub { $_[1] ? $promise->reject($_[1]) : $promise->resolve($_[2]) });
  0            
75              
76 0           return $promise;
77             }
78              
79             sub _write_temp_file {
80 0     0     my ($tempfile_open_mode, $rows, $headers) = @_;
81              
82 0           my $tempfile = tempfile();
83 0           my $temp_fh = $tempfile->open($tempfile_open_mode);
84 0 0         csv(in => $rows, out => $temp_fh, sep_char => "\t", quote_char => q{"}, eol => "\n", headers => $headers) or Carp::croak $text_csv_package->error_diag;
85 0           close $temp_fh;
86              
87 0           return $tempfile;
88             }
89              
90             sub _parse_options {
91 0     0     my %options = @_;
92              
93 0           my @parsed_options = (
94             _parse_low_priority_and_concurrent(\%options),
95             _parse_replace_and_ignore(\%options),
96             _parse_table(\%options),
97             _parse_partition(\%options),
98             _parse_character_set_and_tempfile_open_mode(\%options),
99             _parse_set(\%options),
100             _parse_rows_and_columns_and_headers(\%options),
101             );
102              
103 0 0         Carp::croak 'unknown options provided: ' . Mojo::Util::dumper(\%options) if %options;
104              
105 0           return @parsed_options;
106             }
107              
108             sub _parse_low_priority_and_concurrent {
109 0     0     my ($options) = @_;
110              
111 0 0 0       if ($options->{low_priority} and $options->{concurrent}) {
112 0           Carp::croak 'cannot set both low_priority and concurrent';
113             }
114              
115 0 0         my $low_priority = delete $options->{low_priority} ? 'LOW_PRIORITY' : '';
116 0 0         my $concurrent = delete $options->{concurrent} ? 'CONCURRENT' : '';
117              
118 0           return $low_priority, $concurrent;
119             }
120              
121             sub _parse_replace_and_ignore {
122 0     0     my ($options) = @_;
123              
124 0 0 0       if ($options->{replace} and $options->{ignore}) {
125 0           Carp::croak 'cannot set both replace and ignore';
126             }
127 0 0         my $replace = delete $options->{replace} ? 'REPLACE' : '';
128 0 0         my $ignore = delete $options->{ignore} ? 'IGNORE' : '';
129              
130 0           return $replace, $ignore;
131             }
132              
133             sub _parse_table {
134 0     0     my $table = delete shift->{table};
135 0 0 0       Carp::croak 'table required for load_data_infile' unless defined $table and $table ne '';
136              
137 0           return $table;
138              
139             }
140              
141             sub _parse_partition {
142 0     0     my ($options) = @_;
143              
144 0           my $partition = delete $options->{partition};
145 0   0       my $ref = ref $partition // '';
146 0 0 0       Carp::croak 'partition must be an arrayref if provided' if $partition and $ref ne 'ARRAY';
147              
148 0 0 0       return $partition && @$partition ? 'PARTITION (' . join(',', map "`$_`", @$partition) . ')' : '';
149             }
150              
151             sub _parse_character_set_and_tempfile_open_mode {
152 0     0     my ($options) = @_;
153              
154 0 0 0       if ($options->{character_set} xor $options->{tempfile_open_mode}) {
155 0           Carp::croak 'character_set and tempfile_open_mode must both be set when one is';
156             }
157              
158 0   0       my $character_set = delete $options->{character_set} || 'utf8';
159 0   0       my $tempfile_open_mode = delete $options->{tempfile_open_mode} || '>:encoding(UTF-8)';
160              
161 0           return $character_set, $tempfile_open_mode;
162             }
163              
164             sub _parse_set {
165 0     0     my ($options) = @_;
166              
167 0           my $set = delete $options->{set};
168 0   0       my $ref = ref $set // '';
169 0 0 0       Carp::croak 'set must be an arrayref if provided' if $set and $ref ne 'ARRAY';
170              
171             return
172             $set && @$set
173             ? 'SET ' . join ',', map {
174 0 0 0       Carp::croak 'hashrefs passed to set must have only exactly key and value' unless keys %$_ == 1;
  0 0          
175 0           my ($column, $expression) = %$_;
176 0           "`$column`=$expression"
177             } @$set
178             : '';
179             }
180              
181             sub _parse_rows_and_columns_and_headers {
182 0     0     my ($options) = @_;
183              
184 0   0       my $rows = delete $options->{rows} || Carp::croak 'rows required for load_data_infile';
185 0 0         Carp::croak 'rows must be an arrayref' unless ref $rows eq 'ARRAY';
186 0 0         Carp::croak 'rows cannot be empty' unless @$rows;
187              
188 0           my $hashes_in_columns_allowed;
189 0 0         if (ref $rows->[0] eq 'ARRAY') {
190             Carp::croak 'columns required when rows contains arrayrefs'
191 0 0 0       unless $options->{columns} and @{$options->{columns}};
  0            
192             } else {
193 0           $hashes_in_columns_allowed = 1;
194             }
195              
196 0 0 0       Carp::croak 'columns array cannot be empty' if $options->{columns} and not @{$options->{columns}};
  0            
197 0   0       my $columns = delete $options->{columns} || [keys %{$rows->[0]}];
198             my @headers = map {
199 0           my $header;
  0            
200 0 0 0       if (ref $_ and ref $_ eq 'HASH') {
201 0 0         Carp::croak 'cannot provide hashes in columns when rows contains arrayrefs' unless $hashes_in_columns_allowed;
202 0 0         Carp::croak 'hashrefs passed to columns must have only one key and value' unless keys %$_ == 1;
203 0           ($header) = keys %$_;
204             } else {
205 0           $header = $_;
206             }
207              
208 0 0 0       Carp::croak 'columns elements cannot be undef or an empty string' unless defined $header and $header ne '';
209              
210 0           $header;
211             } @$columns;
212             $columns = join ',', map {
213 0           my $column;
  0            
214 0 0 0       if (ref $_ and ref $_ eq 'HASH') {
215 0 0         Carp::croak 'cannot provide hashes in columns when rows contains arrayrefs' unless $hashes_in_columns_allowed;
216 0 0         Carp::croak 'hashrefs passed to columns must have only exactly key and value' unless keys %$_ == 1;
217 0           ($column) = values %$_;
218             } else {
219 0           $column = $_;
220             }
221              
222 0 0 0       Carp::croak 'columns elements cannot be undef or an empty string' unless defined $column and $column ne '';
223              
224 0           "`$column`";
225             } @$columns;
226              
227 0           return $rows, $columns, \@headers;
228             }
229              
230             sub _build_query {
231 0     0     my ($low_priority, $concurrent, $tempfile, $replace, $ignore, $table, $partition, $character_set, $columns, $set) = @_;
232              
233 0           return qq{
234             LOAD DATA $low_priority $concurrent LOCAL INFILE '$tempfile'
235             $replace $ignore INTO TABLE `$table`
236             $partition
237             CHARACTER SET '$character_set'
238             FIELDS TERMINATED BY '\\t' OPTIONALLY ENCLOSED BY '"'
239             LINES TERMINATED BY '\\n'
240             IGNORE 1 LINES
241             ($columns)
242             $set
243             };
244             }
245              
246             1;
247             __END__