a542c924318ed668dacabc5e7e945fdead3b6cd5
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / Dumper.pm
1 package SQL::Translator::Producer::Dumper;
2
3 # -------------------------------------------------------------------
4 # $Id: Dumper.pm 1440 2009-01-17 16:31:57Z jawnsy $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-2006 SQLFairy Authors
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation; version 2.
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20 # 02111-1307  USA
21 # -------------------------------------------------------------------
22
23 =head1 NAME
24
25 SQL::Translator::Producer::Dumper - SQL Dumper producer for SQL::Translator
26
27 =head1 SYNOPSIS
28
29   use SQL::Translator::Producer::Dumper;
30
31   Options:
32
33     db_user         Database username
34     db_password     Database password
35     dsn             DSN for DBI
36     mysql_loadfile  Create MySQL's LOAD FILE syntax instead of INSERTs
37     skip=t1[,t2]    Skip tables in comma-separated list
38     skiplike=regex  Skip tables in comma-separated list
39
40 =head1 DESCRIPTION
41
42 This producer creates a Perl script that can connect to a database and
43 dump the data as INSERT statements (a la mysqldump) or as a file
44 suitable for MySQL's LOAD DATA command.  If you enable "add-truncate"
45 or specify tables to "skip" (also using the "skiplike" regular
46 expression) then the generated dumper script will leave out those
47 tables.  However, these will also be options in the generated dumper,
48 so you can wait to specify these options when you dump your database.
49 The database username, password, and DSN can be hardcoded into the
50 generated script, or part of the DSN can be intuited from the
51 "database" argument.
52
53 =cut
54
55 use strict;
56 use Config;
57 use SQL::Translator;
58 use File::Temp 'tempfile';
59 use Template;
60
61 use Data::Dumper;
62
63 sub produce {
64     my $t              = shift;
65     my $args           = $t->producer_args;
66     my $schema         = $t->schema;
67     my $add_truncate   = $args->{'add_truncate'}   || 0; 
68     my $skip           = $args->{'skip'}           || '';
69     my $skiplike       = $args->{'skiplike'}       || '';
70     my $db_user        = $args->{'db_user'}        || 'db_user';
71     my $db_pass        = $args->{'db_password'}    || 'db_pass';
72     my $parser_name    = $t->parser_type;
73     my %skip           = map { $_, 1 } map { s/^\s+|\s+$//; $_ } 
74                          split (/,/, $skip);
75     my $sqlt_version   = $t->version;
76
77     if ( $parser_name  =~ /Parser::(\w+)$/ ) { 
78         $parser_name = $1 
79     }
80
81     my %type_to_dbd    = (
82         MySQL          => 'mysql',
83         Oracle         => 'Oracle',
84         PostgreSQL     => 'Pg',
85         SQLite         => 'SQLite',
86         Sybase         => 'Sybase',
87     );
88     my $dbd            = $type_to_dbd{ $parser_name } || 'DBD';
89     my $dsn            = $args->{'dsn'} || "dbi:$dbd:";
90     if ( $dbd eq 'Pg' && ! $args->{'dsn'} ) {
91         $dsn .= 'dbname=dbname;host=hostname';
92     }
93     elsif ( $dbd eq 'Oracle' && ! $args->{'dsn'} ) {
94         $db_user = "$db_user/$db_pass@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)" .
95             "(HOST=hostname)(PORT=1521))(CONNECT_DATA=(SID=sid)))";
96         $db_pass = '';
97     }
98     elsif ( $dbd eq 'mysql' && ! $args->{'dsn'} ) {
99         $dsn .= 'dbname';
100     }
101
102     my $template      = Template->new;
103     my $template_text = template();
104     my $out;
105     $template->process( 
106         \$template_text, 
107         { 
108             translator     => $t,
109             schema         => $schema,
110             db_user        => $db_user,
111             db_pass        => $db_pass,
112             dsn            => $dsn,
113             perl           => $Config{'startperl'},
114             skip           => \%skip,
115             skiplike       => $skiplike,
116         }, 
117         \$out 
118     ) or die $template->error;
119
120     return $out;
121 }
122
123 # -------------------------------------------------------------------
124 sub template {
125 #
126 # Returns the template to be processed by Template Toolkit
127 #
128     return <<'EOF';
129 [% perl || '#!/usr/bin/perl' %]
130 [% USE date %]
131 #
132 # Generated by SQL::Translator [% translator.version %]
133 # [% date.format( date.now, "%Y-%m-%d" ) %]
134 # For more info, see http://sqlfairy.sourceforge.net/
135 #
136
137 use strict;
138 use Cwd;
139 use DBI;
140 use Getopt::Long;
141 use File::Spec::Functions 'catfile';
142
143 my ( $help, $add_truncate, $skip, $skiplike, $no_comments, 
144     $takelike, $mysql_loadfile );
145 GetOptions(
146     'add-truncate'   => \$add_truncate,
147     'h|help'         => \$help,
148     'no-comments'    => \$no_comments,
149     'mysql-loadfile' => \$mysql_loadfile,
150     'skip:s'         => \$skip,
151     'skiplike:s'     => \$skiplike,
152     'takelike:s'     => \$takelike,
153 );
154
155 if ( $help ) {
156     print <<"USAGE";
157 Usage:
158   $0 [options] > dump.sql
159
160   Options:
161     -h|--help          Show help and exit
162     --add-truncate     Add "TRUNCATE TABLE" statements
163     --mysql-loadfile   Create MySQL's LOAD FILE syntax, not INSERTs
164     --no-comments      Suppress comments
165     --skip=t1[,t2]     Comma-separated list of tables to skip
166     --skiplike=regex   Regular expression of table names to skip
167     --takelike=regex   Regular expression of table names to take
168
169 USAGE
170     exit(0);
171 }
172
173 $no_comments = 1 if $mysql_loadfile;
174
175 [%-
176 SET table_defs = [];
177 SET max_field  = 0;
178
179 FOREACH table IN schema.get_tables;
180     SET table_name = table.name;
181     NEXT IF skip.$table_name;
182     NEXT IF skiplike AND table_name.match("(?:$skiplike)");
183
184     SET field_names = [];
185     SET types       = {};
186     FOR field IN table.get_fields;
187         field_name = field.name;
188         fname_len  = field.name.length;
189         max_field  = fname_len > max_field ? fname_len : max_field;
190         types.$field_name = field.data_type.match( '(char|str|long|text|enum|date)' )
191             ? 'string' : 'number';
192         field_names.push( field_name );
193     END;
194
195     table_defs.push({
196         name   => table_name,
197         types  => types,
198         fields => field_names,
199     });
200 END 
201 -%]
202
203 my $db     = DBI->connect(
204     '[% dsn %]', 
205     '[% db_user %]', 
206     '[% db_pass %]', 
207     { RaiseError => 1 }
208 );
209 my %skip   = map { $_, 1 } map { s/^\s+|\s+$//; $_ } split (/,/, $skip);
210 my @tables = (
211 [%- FOREACH t IN table_defs %]
212     {
213         table_name  => '[% t.name %]',
214         fields      => [ qw/ [% t.fields.join(' ') %] / ],
215         types       => {
216             [%- FOREACH fname IN t.types.keys %]
217             '[% fname %]' => '[% t.types.$fname %]',
218             [%- END %]
219         },
220     },
221 [%- END %]
222 );
223
224 for my $table ( @tables ) {
225     my $table_name = $table->{'table_name'};
226     next if $skip{ $table_name };
227     next if $skiplike && $table_name =~ qr/$skiplike/;
228     next if $takelike && $table_name !~ qr/$takelike/;
229
230     my ( $out_fh, $outfile );
231     if ( $mysql_loadfile ) {
232         $outfile = catfile( cwd(), "$table_name.txt" );
233         open $out_fh, ">$outfile" or 
234             die "Can't write LOAD FILE to '$table_name': $!\n";
235     }
236
237     print "--\n-- Data for table '$table_name'\n--\n" unless $no_comments;
238
239     if ( $add_truncate ) {
240         print "TRUNCATE TABLE $table_name;\n";
241     }
242
243     my $sql =
244         'select ' . join(', ', @{ $table->{'fields'} } ) . " from $table_name"
245     ;
246     my $sth = $db->prepare( $sql );
247     $sth->execute;
248
249     while ( my $rec = $sth->fetchrow_hashref ) {
250         my @vals;
251         for my $fld ( @{ $table->{'fields'} } ) {
252             my $val = $rec->{ $fld };
253             if ( $table->{'types'}{ $fld } eq 'string' ) {
254                 if ( defined $val ) {
255                     $val =~ s/'/\\'/g;
256                     $val = qq['$val']
257                 }
258                 else {
259                     $val = qq[''];
260                 }
261             }
262             else {
263                 $val = defined $val ? $val : $mysql_loadfile ? '\N' : 'NULL';
264             }
265             push @vals, $val;
266         }        
267
268         if ( $mysql_loadfile ) {
269             print $out_fh join("\t", @vals), "\n";
270         }
271         else {
272             print "INSERT INTO $table_name (".
273                 join(', ', @{ $table->{'fields'} }) . 
274                 ') VALUES (', join(', ', @vals), ");\n";
275         }
276     }
277
278     if ( $out_fh ) {
279         print "LOAD DATA INFILE '$outfile' INTO TABLE $table_name ",
280             "FIELDS OPTIONALLY ENCLOSED BY '\\'';\n";
281         close $out_fh or die "Can't close filehandle: $!\n";
282     }
283     else {
284         print "\n";
285     }
286 }
287 EOF
288 }
289
290 1;
291
292 # -------------------------------------------------------------------
293 # To create a little flower is the labour of ages.
294 # William Blake
295 # -------------------------------------------------------------------
296
297 =pod
298
299 =head1 AUTHOR
300
301 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
302
303 =cut