File Coverage

blib/lib/ETL/Yertl/Command/ysql.pm
Criterion Covered Total %
statement 157 180 87.2
branch 71 88 80.6
condition 16 20 80.0
subroutine 12 12 100.0
pod 0 5 0.0
total 256 305 83.9


line stmt bran cond sub pod time code
1             package ETL::Yertl::Command::ysql;
2             our $VERSION = '0.036';
3             # ABSTRACT: Read and write documents with a SQL database
4              
5 1     1   6 use ETL::Yertl;
  1         1  
  1         12  
6 1     1   29 use ETL::Yertl::Util qw( load_module );
  1         2  
  1         36  
7 1     1   5 use Getopt::Long qw( GetOptionsFromArray :config pass_through );
  1         2  
  1         4  
8 1     1   382 use File::HomeDir;
  1         3624  
  1         48  
9 1     1   5 use Path::Tiny qw( tempfile );
  1         2  
  1         32  
10 1     1   454 use SQL::Abstract;
  1         7563  
  1         914  
11              
12             sub main {
13 39     39 0 88 my $class = shift;
14              
15 39         56 eval { require DBI; };
  39         245  
16 39 50       128 if ( $@ ) {
17 0         0 die "Can't load ysql: Can't load DBI. Make sure the DBI module is installed.\n";
18             }
19              
20 39         69 my %opt;
21 39 50       121 if ( ref $_[-1] eq 'HASH' ) {
22 39         64 %opt = %{ pop @_ };
  39         110  
23             }
24              
25 39         106 my @args = @_;
26 39         159 GetOptionsFromArray( \@args, \%opt,
27             'config',
28             'drivers',
29             'driver|t=s',
30             'database|db=s',
31             'host|h=s',
32             'port|p=s',
33             'user|u=s',
34             'password|pass=s',
35             'save=s',
36             'edit|e=s',
37             'select=s',
38             'count=s',
39             'insert=s',
40             'delete=s',
41             'where=s',
42             'order|order-by|sort=s',
43             );
44             #; use Data::Dumper;
45             #; say Dumper \@args;
46             #; say Dumper \%opt;
47              
48 39         43486 my $out_fmt = load_module( format => 'default' )->new;
49              
50 39 100       155 if ( $opt{config} ) {
    100          
51 16         29 my $db_key = shift @args;
52              
53 16 100       38 if ( !$db_key ) {
54 1         4 my $out_fmt = load_module( format => 'yaml' )->new;
55 1         4 print $out_fmt->write( config() );
56 1         9 return 0;
57             }
58              
59             # Get the existing config first
60 15         35 my $db_conf = db_config( $db_key );
61              
62 15 100 100     64 if ( !@args && !grep { defined } @opt{qw( dsn driver database host port user password )} ) {
  70         113  
63 3 100       20 die "Database key '$db_key' does not exist" unless keys %$db_conf;
64 2         5 my $out_fmt = load_module( format => 'yaml' )->new;
65 2         6 print $out_fmt->write( $db_conf );
66 2         13 return 0;
67             }
68              
69             #; use Data::Dumper;
70             #; say "Got from options: " . Dumper $db_conf;
71             #; say "Left in \@args: " . Dumper \@args;
72              
73 12         26 for my $key ( qw{ driver database host port user password } ) {
74 72 100       124 next if !$opt{ $key };
75 17         31 $db_conf->{ $key } = $opt{ $key };
76             }
77              
78             # Set via DSN
79 12 100 100     45 if ( my $dsn = $opt{dsn} || shift( @args ) ) {
80 7         22 delete $db_conf->{ $_ } for qw( driver database host port );
81 7         44 my ( undef, $driver, undef, undef, $driver_dsn ) = DBI->parse_dsn( $dsn );
82 7         144 $db_conf->{ driver } = $driver;
83              
84             # The driver_dsn part is up to the driver, but we can make some guesses
85 7 100       47 if ( $driver_dsn !~ /[=:;@]/ ) {
    50          
    50          
86 5         11 $db_conf->{ database } = $driver_dsn;
87             }
88             elsif ( $driver_dsn =~ /^(\w+)\@([\w.]+)(?:\:(\d+))?$/ ) {
89 0         0 $db_conf->{ database } = $1;
90 0         0 $db_conf->{ host } = $2;
91 0         0 $db_conf->{ port } = $3;
92             }
93             elsif ( my @parts = split /\;/, $driver_dsn ) {
94 2         5 for my $part ( @parts ) {
95 5         12 my ( $part_key, $part_value ) = split /=/, $part;
96 5 50       9 if ( $part_key eq 'dbname' ) {
97 0         0 $part_key = 'database';
98             }
99 5         10 $db_conf->{ $part_key } = $part_value;
100             }
101             }
102             else {
103 0         0 die "Unknown driver DSN: $driver_dsn";
104             }
105             }
106              
107             # Check if the driver is installed
108 12         21 my $driver = $db_conf->{driver};
109 12 100       53 if ( !grep { /^$driver$/ } DBI->available_drivers ) {
  84         3332  
110 6         24 my @possible = grep { /^$driver$/i } DBI->available_drivers;
  42         1273  
111 6 100       19 my $suggest = @possible ? " Did you mean: $possible[0]" : '';
112 6         151 warn "Driver '$driver' does not exist." . $suggest . "\n";
113             }
114              
115             # Write back the config
116 12         37 db_config( $db_key => $db_conf );
117              
118             }
119             elsif ( $opt{drivers} ) {
120 1         4 my $ignore = join "|", qw( ExampleP Sponge File );
121 1         8 say join "\n", grep { !/^(?:$ignore)$/ } DBI->available_drivers;
  7         323  
122              
123             }
124             else {
125 22 50       65 if ( $opt{ edit } ) {
126 0         0 my $db_key = shift @args;
127 0         0 my $db_conf = db_config( $db_key );
128 0         0 my $query = $db_conf->{query}{ $opt{edit} };
129 0         0 my $tmp = tempfile;
130 0         0 $tmp->spew( $query );
131 0         0 system $ENV{EDITOR}, "$tmp";
132 0         0 $db_conf->{query}{ $opt{edit} } = $tmp->slurp;
133 0         0 db_config( $db_key => $db_conf );
134 0         0 return 0;
135             }
136              
137 22 100       57 if ( $opt{ save } ) {
138 2         4 my $db_key = shift @args;
139 2         10 my $db_conf = db_config( $db_key );
140 2         11 $db_conf->{query}{ $opt{save} } = shift @args;
141 2         9 db_config( $db_key => $db_conf );
142 2         12 return 0;
143             }
144              
145 20 100       66 my $db_key = !$opt{dsn} ? shift @args : undef;
146 20 100 100     63 if ( !$db_key && !$opt{dsn} ) {
147 1         11 die "Must specify a database!\n";
148             }
149              
150 19 100       92 my @dbi_args = $opt{dsn} ? ( $opt{dsn}, undef, undef ) : dbi_args( $db_key );
151 19 50       68 if ( !@dbi_args ) {
152 0         0 die "Unknown database '$db_key'\n";
153             }
154              
155 19         166 my $dbh = DBI->connect( @dbi_args, { PrintError => 0 } );
156 19 100       7086 if ( !$dbh ) {
157 1     1   7 no warnings 'once';
  1         3  
  1         1219  
158 1 50       19 die sprintf qq{Could not connect to database "\%s"\%s: \%s\n},
159             $dbi_args[0],
160             $dbi_args[1] ? qq{ (user: "$dbi_args[1]")} : '',
161             $DBI::errstr;
162             }
163              
164 18         149 my $sql = SQL::Abstract->new;
165              
166             # Insert helper requires special handling, as the query may change
167             # with every document inserted.
168 18 100       976 if ( $opt{insert} ) {
169 2 50 33     40 if ( !-t *STDIN && !-z *STDIN ) {
170 2         12 my $in_fmt = load_module( format => 'default' )->new( input => \*STDIN );
171              
172 2         10 my $query;
173             my @bind_args;
174 2         0 my $sth;
175 2         9 for my $doc ( $in_fmt->read ) {
176 3 100       15 if ( grep { ref } values %$doc ) {
  9         27  
177 1         12 die q{Can't insert complex data structures using '--insert'. Please use SQL with '$' placeholders instead}."\n";
178             }
179              
180 2         10 my ( $new_query, @bind_args ) = $sql->insert( $opt{insert}, $doc );
181 2 100 66     683 if ( !$query || $new_query ne $query ) {
182 1         2 $query = $new_query;
183 1 50       6 $sth = $dbh->prepare( $query )
184             or die "SQL error in prepare: " . $dbh->errstr . "\n";
185             }
186              
187 2 50       14780 $sth->execute( @bind_args )
188             or die "SQL error in execute: " . $dbh->errstr . "\n";
189 2         67 while ( my $doc = $sth->fetchrow_hashref ) {
190 0         0 print $out_fmt->write( $doc );
191             }
192             }
193              
194             }
195             else {
196 0         0 my ( $query, @bind_args ) = $sql->insert( $opt{insert}, \@args );
197 0 0       0 my $sth = $dbh->prepare( $query )
198             or die "SQL error in prepare: " . $dbh->errstr . "\n";
199              
200 0 0       0 $sth->execute( @bind_args )
201             or die "SQL error in execute: " . $dbh->errstr . "\n";
202 0         0 while ( my $doc = $sth->fetchrow_hashref ) {
203 0         0 print $out_fmt->write( $doc );
204             }
205             }
206 1         7 return 0;
207             }
208              
209             # Other queries that do not require special handling
210 16         29 my $query;
211 16 100       69 if ( $opt{select} ) {
    100          
    100          
212 4         18 $query = $sql->select( $opt{select}, '*', $opt{where}, $opt{order} );
213             }
214             elsif ( $opt{count} ) {
215 2         11 $query = $sql->select( $opt{count}, 'COUNT(*) AS value', $opt{where} );
216             }
217             elsif ( $opt{delete} ) {
218 2         10 $query = $sql->delete( $opt{delete}, $opt{where} );
219             }
220             else {
221 8         14 $query = shift @args;
222              
223             # Check for saved query
224 8 100       24 if ( $db_key ) {
225 7         23 my $db_conf = db_config( $db_key );
226 7 100       34 if ( $db_conf->{query}{ $query } ) {
227 2         8 $query = $db_conf->{query}{ $query };
228             }
229             }
230             }
231              
232             # Resolve interpolations with placeholders
233 16         1325 my @fields = $query =~ m/\$(\.[.\w]+)/g;
234 16         51 $query =~ s/\$\.[\w.]+/?/g;
235              
236 16 100       95 my $sth = $dbh->prepare( $query )
237             or die "SQL error in prepare: " . $dbh->errstr . "\n";
238              
239 14 100 66     3842 if ( !-t *STDIN && !-z *STDIN ) {
240 2         8 my $in_fmt = load_module( format => 'default' )->new( input => \*STDIN );
241              
242 2         7 for my $doc ( $in_fmt->read ) {
243 4 50       15 $sth->execute( map { select_doc( $_, $doc ) } @fields )
  12         28  
244             or die "SQL error in execute: " . $dbh->errstr . "\n";
245 4         128 while ( my $doc = $sth->fetchrow_hashref ) {
246 0         0 print $out_fmt->write( $doc );
247             }
248             }
249              
250             }
251             else {
252 12 100       11532 $sth->execute( @args )
253             or die "SQL error in execute: " . $dbh->errstr . "\n";
254 11         274 while ( my $doc = $sth->fetchrow_hashref ) {
255 14         60 print $out_fmt->write( $doc );
256             }
257             }
258              
259 13         227 return 0;
260             }
261             }
262              
263             sub config {
264 39     39 0 164 my $conf_file = path( File::HomeDir->my_home, '.yertl', 'ysql.yml' );
265 39         2085 my $config = {};
266 39 100       97 if ( $conf_file->exists ) {
267 33         357 my $yaml = load_module( format => 'yaml' )->new( input => $conf_file->openr );
268 33         108 ( $config ) = $yaml->read;
269             }
270 39         279 return $config;
271             }
272              
273             sub db_config {
274 38     38 0 83 my ( $db_key, $config ) = @_;
275 38 100       70 if ( $config ) {
276 14         64 my $conf_file = path( File::HomeDir->my_home, '.yertl', 'ysql.yml' );
277 14 100       740 if ( !$conf_file->exists ) {
278 6         49 $conf_file->touchpath;
279             }
280 14         2183 my $all_config = config();
281 14         38 $all_config->{ $db_key } = $config;
282 14         32 my $yaml = load_module( format => 'yaml' )->new;
283 14         44 $conf_file->spew( $yaml->write( $all_config ) );
284 14         4591 return;
285             }
286 24   100     51 return config()->{ $db_key } || {};
287             }
288              
289             sub select_doc {
290 12     12 0 23 my ( $select, $doc ) = @_;
291 12         45 $select =~ s/^[.]//; # select must start with .
292 12         30 my @parts = split /[.]/, $select;
293 12         23 for my $part ( @parts ) {
294 14         28 $doc = $doc->{ $part };
295             }
296 12         26720 return $doc;
297             }
298              
299             sub dbi_args {
300 17     17 0 50 my ( $db_name ) = @_;
301 17         107 my $conf_file = path( File::HomeDir->my_home, '.yertl', 'ysql.yml' );
302 17 50       1183 if ( $conf_file->exists ) {
303 17         244 my $yaml = load_module( format => 'yaml' )->new( input => $conf_file->openr );
304 17         81 my ( $config ) = $yaml->read;
305 17         50 my $db_config = $config->{ $db_name };
306              
307             my $driver_dsn =
308             join ";",
309 17         76 map { join "=", $_, $db_config->{ $_ } }
310 17         42 grep { $db_config->{ $_ } }
  51         103  
311             qw( database host port )
312             ;
313              
314             return (
315             sprintf( 'dbi:%s:%s', $db_config->{driver}, $driver_dsn ),
316             $db_config->{user},
317             $db_config->{password},
318 17         279 );
319             }
320             }
321              
322             1;
323              
324             __END__