f18cd5de943e8b6b711eddad11cebc5ca2035415
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / Dumper.pm
1 package SQL::Translator::Producer::Dumper;
2
3 # -------------------------------------------------------------------
4 # $Id$
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-6 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 use vars qw($VERSION);
61
62 use Data::Dumper;
63
64 $VERSION = sprintf "%d.%02d", q$Revision$ =~ /(\d+)\.(\d+)/;
65
66 sub produce {
67     my $t              = shift;
68     my $args           = $t->producer_args;
69     my $schema         = $t->schema;
70     my $add_truncate   = $args->{'add_truncate'}   || 0; 
71     my $skip           = $args->{'skip'}           || '';
72     my $skiplike       = $args->{'skiplike'}       || '';
73     my $db_user        = $args->{'db_user'}        || 'db_user';
74     my $db_pass        = $args->{'db_password'}    || 'db_pass';
75     my $parser_name    = $t->parser_type;
76     my %skip           = map { $_, 1 } map { s/^\s+|\s+$//; $_ } 
77                          split (/,/, $skip);
78     my $sqlt_version   = $t->version;
79
80     if ( $parser_name  =~ /Parser::(\w+)$/ ) { 
81         $parser_name = $1 
82     }
83
84     my %type_to_dbd    = (
85         MySQL          => 'mysql',
86         Oracle         => 'Oracle',
87         PostgreSQL     => 'Pg',
88         SQLite         => 'SQLite',
89         Sybase         => 'Sybase',
90     );
91     my $dbd            = $type_to_dbd{ $parser_name } || 'DBD';
92     my $dsn            = $args->{'dsn'} || "dbi:$dbd:";
93     if ( $dbd eq 'Pg' && ! $args->{'dsn'} ) {
94         $dsn .= 'dbname=dbname;host=hostname';
95     }
96     elsif ( $dbd eq 'Oracle' && ! $args->{'dsn'} ) {
97         $db_user = "$db_user/$db_pass@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)" .
98             "(HOST=hostname)(PORT=1521))(CONNECT_DATA=(SID=sid)))";
99         $db_pass = '';
100     }
101     elsif ( $dbd eq 'mysql' && ! $args->{'dsn'} ) {
102         $dsn .= 'dbname';
103     }
104
105     my $template      = Template->new;
106     my $template_text = template();
107     my $out;
108     $template->process( 
109         \$template_text, 
110         { 
111             translator     => $t,
112             schema         => $schema,
113             db_user        => $db_user,
114             db_pass        => $db_pass,
115             dsn            => $dsn,
116             perl           => $Config{'startperl'},
117             skip           => \%skip,
118             skiplike       => $skiplike,
119         }, 
120         \$out 
121     ) or die $template->error;
122
123     return $out;
124 }
125
126 # -------------------------------------------------------------------
127 sub template {
128 #
129 # Returns the template to be processed by Template Toolkit
130 #
131     return <<'EOF';
132 [% perl || '#!/usr/bin/perl' %]
133 [% USE date %]
134 #
135 # Generated by SQL::Translator [% translator.version %]
136 # [% date.format( date.now, "%Y-%m-%d" ) %]
137 # For more info, see http://sqlfairy.sourceforge.net/
138 #
139
140 use strict;
141 use Cwd;
142 use DBI;
143 use Getopt::Long;
144 use File::Spec::Functions 'catfile';
145
146 my ( $help, $add_truncate, $skip, $skiplike, $no_comments, 
147     $takelike, $mysql_loadfile );
148 GetOptions(
149     'add-truncate'   => \$add_truncate,
150     'h|help'         => \$help,
151     'no-comments'    => \$no_comments,
152     'mysql-loadfile' => \$mysql_loadfile,
153     'skip:s'         => \$skip,
154     'skiplike:s'     => \$skiplike,
155     'takelike:s'     => \$takelike,
156 );
157
158 if ( $help ) {
159     print <<"USAGE";
160 Usage:
161   $0 [options] > dump.sql
162
163   Options:
164     -h|--help          Show help and exit
165     --add-truncate     Add "TRUNCATE TABLE" statements
166     --mysql-loadfile   Create MySQL's LOAD FILE syntax, not INSERTs
167     --no-comments      Suppress comments
168     --skip=t1[,t2]     Comma-separated list of tables to skip
169     --skiplike=regex   Regular expression of table names to skip
170     --takelike=regex   Regular expression of table names to take
171
172 USAGE
173     exit(0);
174 }
175
176 $no_comments = 1 if $mysql_loadfile;
177
178 [%-
179 SET table_defs = [];
180 SET max_field  = 0;
181
182 FOREACH table IN schema.get_tables;
183     SET table_name = table.name;
184     NEXT IF skip.$table_name;
185     NEXT IF skiplike AND table_name.match("(?:$skiplike)");
186
187     SET field_names = [];
188     SET types       = {};
189     FOR field IN table.get_fields;
190         field_name = field.name;
191         fname_len  = field.name.length;
192         max_field  = fname_len > max_field ? fname_len : max_field;
193         types.$field_name = field.data_type.match( '(char|str|long|text|enum|date)' )
194             ? 'string' : 'number';
195         field_names.push( field_name );
196     END;
197
198     table_defs.push({
199         name   => table_name,
200         types  => types,
201         fields => field_names,
202     });
203 END 
204 -%]
205
206 my $db     = DBI->connect(
207     '[% dsn %]', 
208     '[% db_user %]', 
209     '[% db_pass %]', 
210     { RaiseError => 1 }
211 );
212 my %skip   = map { $_, 1 } map { s/^\s+|\s+$//; $_ } split (/,/, $skip);
213 my @tables = (
214 [%- FOREACH t IN table_defs %]
215     {
216         table_name  => '[% t.name %]',
217         fields      => [ qw/ [% t.fields.join(' ') %] / ],
218         types       => {
219             [%- FOREACH fname IN t.types.keys %]
220             '[% fname %]' => '[% t.types.$fname %]',
221             [%- END %]
222         },
223     },
224 [%- END %]
225 );
226
227 for my $table ( @tables ) {
228     my $table_name = $table->{'table_name'};
229     next if $skip{ $table_name };
230     next if $skiplike && $table_name =~ qr/$skiplike/;
231     next if $takelike && $table_name !~ qr/$takelike/;
232
233     my ( $out_fh, $outfile );
234     if ( $mysql_loadfile ) {
235         $outfile = catfile( cwd(), "$table_name.txt" );
236         open $out_fh, ">$outfile" or 
237             die "Can't write LOAD FILE to '$table_name': $!\n";
238     }
239
240     print "--\n-- Data for table '$table_name'\n--\n" unless $no_comments;
241
242     if ( $add_truncate ) {
243         print "TRUNCATE TABLE $table_name;\n";
244     }
245
246     my $sql =
247         'select ' . join(', ', @{ $table->{'fields'} } ) . " from $table_name"
248     ;
249     my $sth = $db->prepare( $sql );
250     $sth->execute;
251
252     while ( my $rec = $sth->fetchrow_hashref ) {
253         my @vals;
254         for my $fld ( @{ $table->{'fields'} } ) {
255             my $val = $rec->{ $fld };
256             if ( $table->{'types'}{ $fld } eq 'string' ) {
257                 if ( defined $val ) {
258                     $val =~ s/'/\\'/g;
259                     $val = qq['$val']
260                 }
261                 else {
262                     $val = qq[''];
263                 }
264             }
265             else {
266                 $val = defined $val ? $val : $mysql_loadfile ? '\N' : 'NULL';
267             }
268             push @vals, $val;
269         }        
270
271         if ( $mysql_loadfile ) {
272             print $out_fh join("\t", @vals), "\n";
273         }
274         else {
275             print "INSERT INTO $table_name (".
276                 join(', ', @{ $table->{'fields'} }) . 
277                 ') VALUES (', join(', ', @vals), ");\n";
278         }
279     }
280
281     if ( $out_fh ) {
282         print "LOAD DATA INFILE '$outfile' INTO TABLE $table_name ",
283             "FIELDS OPTIONALLY ENCLOSED BY '\\'';\n";
284         close $out_fh or die "Can't close filehandle: $!\n";
285     }
286     else {
287         print "\n";
288     }
289 }
290 EOF
291 }
292
293 1;
294
295 # -------------------------------------------------------------------
296 # To create a little flower is the labour of ages.
297 # William Blake
298 # -------------------------------------------------------------------
299
300 =pod
301
302 =head1 AUTHOR
303
304 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
305
306 =cut