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