Strip evil svn:keywords
[dbsrgits/SQL-Translator.git] / bin / sqlt-diff-old
CommitLineData
da5a1bae 1#!/usr/bin/perl -w
2# vim: set ft=perl:
3
4# -------------------------------------------------------------------
d4f84dd1 5# $Id: sqlt-diff-old 1440 2009-01-17 16:31:57Z jawnsy $
da5a1bae 6# -------------------------------------------------------------------
478f608d 7# Copyright (C) 2002-2009 The SQLFairy Authors
da5a1bae 8#
9# This program is free software; you can redistribute it and/or
10# modify it under the terms of the GNU General Public License as
11# published by the Free Software Foundation; version 2.
12#
13# This program is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21# 02111-1307 USA
22# -------------------------------------------------------------------
23
24=head1 NAME
25
26sqlt-diff - find the differences b/w two schemas
27
28=head1 SYNOPSIS
29
30For help:
31
32 sqlt-diff -h|--help
33
34For a list of all valid parsers:
35
36 sqlt -l|--list
37
38To diff two schemas:
39
40 sqlt-diff [options] file_name1=parser file_name2=parser
41
42Options:
43
44 -d|--debug Show debugging info
45
46=head1 DESCRIPTION
47
48sqlt-diff is a utility for creating a file of SQL commands necessary to
49transform the first schema provided to the second. While not yet
50exhaustive in its ability to mutate the entire schema, it will report the
51following
52
53=over
54
55=item * New tables
56
57Using the Producer class of the target (second) schema, any tables missing
58in the first schema will be generated in their entirety (fields, constraints,
59indices).
60
61=item * Missing/altered fields
62
63Any fields missing or altered between the two schemas will be reported
64as:
65
66 ALTER TABLE <table_name>
67 [DROP <field_name>]
68 [CHANGE <field_name> <datatype> (<size>)] ;
69
70=item * Missing/altered indices
71
72Any indices missing or of a different type or on different fields will be
73indicated. Indices that should be dropped will be reported as such:
74
75 DROP INDEX <index_name> ON <table_name> ;
76
77An index of a different type or on different fields will be reported as a
78new index as such:
79
80 CREATE [<index_type>] INDEX [<index_name>] ON <table_name>
81 ( <field_name>[,<field_name>] ) ;
82
83=back
84
85"ALTER/DROP TABLE" and "CREATE INDEX" statements B<are not> generated by
86the Producer, unfortunately, and may require massaging before being passed to
87your target database.
88
89=cut
90
91# -------------------------------------------------------------------
92
93use strict;
94use Pod::Usage;
95use Data::Dumper;
96use SQL::Translator;
97use SQL::Translator::Schema::Constants;
98
da5a1bae 99my ( @input, $list, $help, $debug );
100for my $arg ( @ARGV ) {
101 if ( $arg =~ m/^-?-l(ist)?$/ ) {
102 $list = 1;
103 }
104 elsif ( $arg =~ m/^-?-h(elp)?$/ ) {
105 $help = 1;
106 }
107 elsif ( $arg =~ m/^-?-d(ebug)?$/ ) {
108 $debug = 1;
109 }
110 elsif ( $arg =~ m/^([^=]+)=(.+)$/ ) {
111 push @input, { file => $1, parser => $2 };
112 }
113 else {
114 pod2usage( msg => "Unknown argument '$arg'" );
115 }
116}
117
118pod2usage(1) if $help;
119pod2usage('Please specify only two schemas to diff') if scalar @input > 2;
120
121my $tr = SQL::Translator->new;
122my @parsers = $tr->list_parsers;
123my %valid_parsers = map { $_, 1 } @parsers;
124
125if ( $list ) {
126 print "\nParsers:\n", map { "\t$_\n" } sort @parsers;
127 print "\n";
128 exit(0);
129}
130
131pod2usage( msg => 'Too many file args' ) if @input > 2;
132
133my ( $source_schema, $source_db, $target_schema, $target_db );
134
135my $i = 2;
136for my $in ( @input ) {
137 my $file = $in->{'file'};
138 my $parser = $in->{'parser'};
139
140 die "Unable to read file '$file'\n" unless -r $file;
141 die "'$parser' is an invalid parser\n" unless $valid_parsers{ $parser };
142
143 my $t = SQL::Translator->new;
144 $t->debug( $debug );
145 $t->parser( $parser ) or die $tr->error;
146 my $out = $t->translate( $file ) or die $tr->error;
147 my $schema = $t->schema;
148 unless ( $schema->name ) {
149 $schema->name( $file );
150 }
151
152 if ( $i == 1 ) {
153 $source_schema = $schema;
154 $source_db = $parser;
155 }
156 else {
157 $target_schema = $schema;
158 $target_db = $parser;
159 }
160 $i--;
161}
162my $case_insensitive = $target_db =~ /SQLServer/;
163
164my $s1_name = $source_schema->name;
165my $s2_name = $target_schema->name;
166my ( @new_tables, @diffs , @diffs_at_end);
167for my $t1 ( $source_schema->get_tables ) {
168 my $t1_name = $t1->name;
169 my $t2 = $target_schema->get_table( $t1_name, $case_insensitive );
170
171 warn "TABLE '$s1_name.$t1_name'\n" if $debug;
172 unless ( $t2 ) {
173 warn "Couldn't find table '$s1_name.$t1_name' in '$s2_name'\n"
174 if $debug;
175 if ( $target_db =~ /(SQLServer|Oracle)/ ) {
176 for my $constraint ( $t1->get_constraints ) {
177 next if $constraint->type ne FOREIGN_KEY;
178 push @diffs_at_end, "ALTER TABLE $t1_name ADD ".
179 constraint_to_string($constraint, $source_schema).";";
180 $t1->drop_constraint($constraint);
181 }
182 }
183 push @new_tables, $t1;
184 next;
185 }
186
187 # Go through our options
188 my $options_different = 0;
189 my %checkedOptions;
190OPTION:
191 for my $t1_option_ref ( $t1->options ) {
192 my($key1, $value1) = %{$t1_option_ref};
193 for my $t2_option_ref ( $t2->options ) {
194 my($key2, $value2) = %{$t2_option_ref};
195 if ( $key1 eq $key2 ) {
196 if ( defined $value1 != defined $value2 ) {
197 $options_different = 1;
198 last OPTION;
199 }
200 if ( defined $value1 && $value1 ne $value2 ) {
201 $options_different = 1;
202 last OPTION;
203 }
204 $checkedOptions{$key1} = 1;
205 next OPTION;
206 }
207 }
208 $options_different = 1;
209 last OPTION;
210 }
211 # Go through the other table's options
212 unless ( $options_different ) {
213 for my $t2_option_ref ( $t2->options ) {
214 my($key, $value) = %{$t2_option_ref};
215 next if $checkedOptions{$key};
216 $options_different = 1;
217 last;
218 }
219 }
220 # If there's a difference, just re-set all the options
221 my @diffs_table_options;
222 if ( $options_different ) {
223 my @options = ();
224 foreach my $option_ref ( $t1->options ) {
225 my($key, $value) = %{$option_ref};
226 push(@options, defined $value ? "$key=$value" : $key);
227 }
228 my $options = join(' ', @options);
229 @diffs_table_options = ("ALTER TABLE $t1_name $options;");
230 }
231
232 my $t2_name = $t2->name;
233 my(@diffs_table_adds, @diffs_table_changes);
234 for my $t1_field ( $t1->get_fields ) {
235 my $f1_type = $t1_field->data_type;
236 my $f1_size = $t1_field->size;
237 my $f1_name = $t1_field->name;
238 my $f1_nullable = $t1_field->is_nullable;
239 my $f1_default = $t1_field->default_value;
240 my $f1_auto_inc = $t1_field->is_auto_increment;
241 my $t2_field = $t2->get_field( $f1_name, $case_insensitive );
242 my $f1_full_name = "$s1_name.$t1_name.$t1_name";
243 warn "FIELD '$f1_full_name'\n" if $debug;
244
245 my $f2_full_name = "$s2_name.$t2_name.$f1_name";
246
247 unless ( $t2_field ) {
248 warn "Couldn't find field '$f2_full_name' in '$t2_name'\n"
249 if $debug;
250 my $temp_default_value = 0;
251 if ( $target_db =~ /SQLServer/ && !$f1_nullable && !defined $f1_default ) {
252 # SQL Server doesn't allow adding non-nullable, non-default columns
253 # so we add it with a default value, then remove the default value
254 $temp_default_value = 1;
255 my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint);
256 $f1_default = grep($_ eq $f1_type, @numeric_types) ? 0 : '';
257 }
258 push @diffs_table_adds, sprintf( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
259 $t1_name, $target_db =~ /Oracle/ ? '(' : '',
260 $f1_name, $f1_type,
261 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
262 !defined $f1_default ? ''
263 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
264 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
265 : " DEFAULT '$f1_default'",
266 $f1_nullable ? '' : ' NOT NULL',
267 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
268 $target_db =~ /Oracle/ ? ')' : '',
269 );
270 if ( $temp_default_value ) {
271 undef $f1_default;
272 push @diffs_table_adds, sprintf( <<END
273DECLARE \@defname VARCHAR(100), \@cmd VARCHAR(1000)
274SET \@defname =
275(SELECT name
276FROM sysobjects so JOIN sysconstraints sc
277ON so.id = sc.constid
278WHERE object_name(so.parent_obj) = '%s'
279AND so.xtype = 'D'
280AND sc.colid =
281 (SELECT colid FROM syscolumns
282 WHERE id = object_id('%s') AND
283 name = '%s'))
284SET \@cmd = 'ALTER TABLE %s DROP CONSTRAINT '
285+ \@defname
286EXEC(\@cmd)
287END
288 , $t1_name, $t1_name, $f1_name, $t1_name,
289 );
290 }
291 next;
292 }
293
294 my $f2_type = $t2_field->data_type;
295 my $f2_size = $t2_field->size || '';
296 my $f2_nullable = $t2_field->is_nullable;
297 my $f2_default = $t2_field->default_value;
298 my $f2_auto_inc = $t2_field->is_auto_increment;
299 if ( !$t1_field->equals($t2_field, $case_insensitive) ) {
300 # SQLServer timestamp fields can't be altered, so we drop and add instead
301 if ( $target_db =~ /SQLServer/ && $f2_type eq "timestamp" ) {
302 push @diffs_table_changes, "ALTER TABLE $t1_name DROP COLUMN $f1_name;";
303 push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
304 $t1_name, $target_db =~ /Oracle/ ? '(' : '',
305 $f1_name, $f1_type,
306 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
307 !defined $f1_default ? ''
308 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
309 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
310 : " DEFAULT '$f1_default'",
311 $f1_nullable ? '' : ' NOT NULL',
312 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
313 $target_db =~ /Oracle/ ? ')' : '',
314 );
315 next;
316 }
317
318 my $changeText = $target_db =~ /SQLServer/ ? 'ALTER COLUMN' :
319 $target_db =~ /Oracle/ ? 'MODIFY (' : 'CHANGE';
320 my $nullText = $f1_nullable ? '' : ' NOT NULL';
321 $nullText = '' if $target_db =~ /Oracle/ && $f1_nullable == $f2_nullable;
322 push @diffs_table_changes, sprintf( "ALTER TABLE %s %s %s%s %s%s%s%s%s%s;",
323 $t1_name, $changeText,
324 $f1_name, $target_db =~ /MySQL/ ? " $f1_name" : '',
325 $f1_type, ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
326 $nullText,
327 !defined $f1_default || $target_db =~ /SQLServer/ ? ''
328 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
329 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
330 : " DEFAULT '$f1_default'",
331 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
332 $target_db =~ /Oracle/ ? ')' : '',
333 );
334 if ( defined $f1_default && $target_db =~ /SQLServer/ ) {
335 # Adding a column with a default value for SQL Server means adding a
336 # constraint and setting existing NULLs to the default value
337 push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;",
338 $t1_name, $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'DEFAULT NULL'
339 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP'
340 : "DEFAULT '$f1_default'", $f1_name,
341 );
342 push @diffs_table_changes, sprintf( "UPDATE %s SET %s = %s WHERE %s IS NULL;",
343 $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'NULL'
344 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP'
345 : "'$f1_default'", $f1_name,
346 );
347 }
348 }
349 }
350
351 my(%checked_indices, @diffs_index_creates, @diffs_index_drops);
352INDEX:
353 for my $i1 ( $t1->get_indices ) {
354 for my $i2 ( $t2->get_indices ) {
355 if ( $i1->equals($i2, $case_insensitive) ) {
356 $checked_indices{$i2} = 1;
357 next INDEX;
358 }
359 }
360 push @diffs_index_creates, sprintf(
361 "CREATE %sINDEX%s ON %s (%s);",
362 $i1->type eq NORMAL ? '' : $i1->type." ",
363 $i1->name ? " ".$i1->name : '',
364 $t1_name,
365 join(",", $i1->fields),
366 );
367 }
368INDEX2:
369 for my $i2 ( $t2->get_indices ) {
370 next if $checked_indices{$i2};
371 for my $i1 ( $t1->get_indices ) {
372 next INDEX2 if $i2->equals($i1, $case_insensitive);
373 }
374 $target_db =~ /SQLServer/
375 ? push @diffs_index_drops, "DROP INDEX $t1_name.".$i2->name.";"
376 : push @diffs_index_drops, "DROP INDEX ".$i2->name." on $t1_name;";
377 }
378
379 my(%checked_constraints, @diffs_constraint_drops);
380CONSTRAINT:
381 for my $c1 ( $t1->get_constraints ) {
382 next if $source_db =~ /Oracle/ && $c1->type eq UNIQUE && $c1->name =~ /^SYS_/i;
383 for my $c2 ( $t2->get_constraints ) {
384 if ( $c1->equals($c2, $case_insensitive) ) {
385 $checked_constraints{$c2} = 1;
386 next CONSTRAINT;
387 }
388 }
389 push @diffs_at_end, "ALTER TABLE $t1_name ADD ".
390 constraint_to_string($c1, $source_schema).";";
391 }
392CONSTRAINT2:
393 for my $c2 ( $t2->get_constraints ) {
394 next if $checked_constraints{$c2};
395 for my $c1 ( $t1->get_constraints ) {
396 next CONSTRAINT2 if $c2->equals($c1, $case_insensitive);
397 }
398 if ( $c2->type eq UNIQUE ) {
399 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP INDEX ".
400 $c2->name.";";
401 } elsif ( $target_db =~ /SQLServer/ ) {
402 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->name.";";
403 } else {
404 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->type.
405 ($c2->type eq FOREIGN_KEY ? " ".$c2->name : '').";";
406 }
407 }
408
409 push @diffs, @diffs_index_drops, @diffs_constraint_drops,
410 @diffs_table_options, @diffs_table_adds,
411 @diffs_table_changes, @diffs_index_creates;
412}
413
414for my $t2 ( $target_schema->get_tables ) {
415 my $t2_name = $t2->name;
416 my $t1 = $source_schema->get_table( $t2_name, $target_db =~ /SQLServer/ );
417
418 unless ( $t1 ) {
419 if ( $target_db =~ /SQLServer/ ) {
420 for my $constraint ( $t2->get_constraints ) {
421 next if $constraint->type eq PRIMARY_KEY;
422 push @diffs, "ALTER TABLE $t2_name DROP ".$constraint->name.";";
423 }
424 }
425 push @diffs_at_end, "DROP TABLE $t2_name;";
426 next;
427 }
428
429 for my $t2_field ( $t2->get_fields ) {
430 my $f2_name = $t2_field->name;
431 my $t1_field = $t1->get_field( $f2_name );
432 unless ( $t1_field ) {
433 my $modifier = $target_db =~ /SQLServer/ ? "COLUMN " : '';
434 push @diffs, "ALTER TABLE $t2_name DROP $modifier$f2_name;";
435 }
436 }
437}
438
439if ( @new_tables ) {
440 my $dummy_tr = SQL::Translator->new;
441 $dummy_tr->schema->add_table( $_ ) for @new_tables;
442 my $producer = $dummy_tr->producer( $target_db );
443 unshift @diffs, $producer->( $dummy_tr );
444}
445push(@diffs, @diffs_at_end);
446
447if ( @diffs ) {
448 if ( $source_db !~ /^(MySQL|SQLServer|Oracle)$/ ) {
449 unshift(@diffs, "-- Target database $target_db is untested/unsupported!!!");
450 }
451}
452
453print STDERR "sqlt-diff-old is deprecated, please try and use sqlt-diff, and tell us about any problems or patch SQL::Translator::Diff\n";
454
455if ( @diffs ) {
456 print join( "\n",
457 "-- Convert schema '$s2_name' to '$s1_name':\n", @diffs, "\n"
458 );
459 exit(1);
460}
461else {
462 print "There were no differences.\n";
463}
464
465sub constraint_to_string {
466 my $c = shift;
467 my $schema = shift or die "No schema given";
468 my @fields = $c->field_names or return '';
469
470 if ( $c->type eq PRIMARY_KEY ) {
471 if ( $target_db =~ /Oracle/ ) {
472 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
473 'PRIMARY KEY (' . join(', ', @fields). ')';
474 } else {
475 return 'PRIMARY KEY (' . join(', ', @fields). ')';
476 }
477 }
478 elsif ( $c->type eq UNIQUE ) {
479 if ( $target_db =~ /Oracle/ ) {
480 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
481 'UNIQUE (' . join(', ', @fields). ')';
482 } else {
483 return 'UNIQUE '.
484 (defined $c->name ? $c->name.' ' : '').
485 '(' . join(', ', @fields). ')';
486 }
487 }
488 elsif ( $c->type eq FOREIGN_KEY ) {
489 my $def = join(' ',
490 map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY'
491 );
492
493 $def .= ' (' . join( ', ', @fields ) . ')';
494
495 $def .= ' REFERENCES ' . $c->reference_table;
496
497 my @rfields = map { $_ || () } $c->reference_fields;
498 unless ( @rfields ) {
499 my $rtable_name = $c->reference_table;
500 if ( my $ref_table = $schema->get_table( $rtable_name ) ) {
501 push @rfields, $ref_table->primary_key;
502 }
503 else {
504 warn "Can't find reference table '$rtable_name' " .
505 "in schema\n";
506 }
507 }
508
509 if ( @rfields ) {
510 $def .= ' (' . join( ', ', @rfields ) . ')';
511 }
512 else {
513 warn "FK constraint on " . 'some table' . '.' .
514 join('', @fields) . " has no reference fields\n";
515 }
516
517 if ( $c->match_type ) {
518 $def .= ' MATCH ' .
519 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
520 }
521
522 if ( $c->on_delete ) {
523 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
524 }
525
526 if ( $c->on_update ) {
527 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
528 }
529
530 return $def;
531 }
532}
533
534# -------------------------------------------------------------------
535# Bring out number weight & measure in a year of dearth.
536# William Blake
537# -------------------------------------------------------------------
538
539=pod
540
541=head1 AUTHOR
542
543Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
544
545=head1 SEE ALSO
546
547SQL::Translator, L<http://sqlfairy.sourceforge.net>.
548
549=cut