Removed the line that declared YAML to be the producer. This stopped the YAML warnin...
[dbsrgits/SQL-Translator.git] / bin / sqlt-diff
CommitLineData
e12ca55a 1#!/usr/bin/perl -w
2# vim: set ft=perl:
3
4# -------------------------------------------------------------------
2726c5d8 5# $Id: sqlt-diff,v 1.9 2005-07-07 21:30:42 mwz444 Exp $
e12ca55a 6# -------------------------------------------------------------------
daf4f623 7# Copyright (C) 2002-4 The SQLFairy Authors
e12ca55a 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
21f5bf40 40 sqlt-diff [options] file_name1=parser file_name2=parser
e12ca55a 41
42Options:
43
44 -d|--debug Show debugging info
45
46=head1 DESCRIPTION
47
942485ea 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.
e12ca55a 88
89=cut
90
91# -------------------------------------------------------------------
92
93use strict;
94use Pod::Usage;
95use Data::Dumper;
96use SQL::Translator;
942485ea 97use SQL::Translator::Schema::Constants;
e12ca55a 98
99use vars qw( $VERSION );
2726c5d8 100$VERSION = sprintf "%d.%02d", q$Revision: 1.9 $ =~ /(\d+)\.(\d+)/;
e12ca55a 101
102my ( @input, $list, $help, $debug );
103for my $arg ( @ARGV ) {
104 if ( $arg =~ m/^-?-l(ist)?$/ ) {
105 $list = 1;
106 }
107 elsif ( $arg =~ m/^-?-h(elp)?$/ ) {
108 $help = 1;
109 }
110 elsif ( $arg =~ m/^-?-d(ebug)?$/ ) {
111 $debug = 1;
112 }
113 elsif ( $arg =~ m/^([^=]+)=(.+)$/ ) {
114 push @input, { file => $1, parser => $2 };
115 }
116 else {
117 pod2usage( msg => "Unknown argument '$arg'" );
118 }
119}
120
121pod2usage(1) if $help;
942485ea 122pod2usage('Please specify only two schemas to diff') if scalar @input > 2;
e12ca55a 123
124my $tr = SQL::Translator->new;
125my @parsers = $tr->list_parsers;
126my %valid_parsers = map { $_, 1 } @parsers;
127
128if ( $list ) {
129 print "\nParsers:\n", map { "\t$_\n" } sort @parsers;
130 print "\n";
131 exit(0);
132}
133
134pod2usage( msg => 'Too many file args' ) if @input > 2;
135
942485ea 136my ( $source_schema, $source_db, $target_schema, $target_db );
137my $i = 2;
e12ca55a 138for my $in ( @input ) {
139 my $file = $in->{'file'};
140 my $parser = $in->{'parser'};
141
142 die "Unable to read file '$file'\n" unless -r $file;
143 die "'$parser' is an invalid parser\n" unless $valid_parsers{ $parser };
144
145 my $t = SQL::Translator->new;
146 $t->debug( $debug );
147 $t->parser( $parser ) or die $tr->error;
e12ca55a 148 my $out = $t->translate( $file ) or die $tr->error;
149 my $schema = $t->schema;
150 unless ( $schema->name ) {
151 $schema->name( $file );
152 }
153
154 if ( $i == 1 ) {
942485ea 155 $source_schema = $schema;
156 $source_db = $parser;
e12ca55a 157 }
158 else {
942485ea 159 $target_schema = $schema;
160 $target_db = $parser;
e12ca55a 161 }
942485ea 162 $i--;
e12ca55a 163}
d71512a6 164my $case_insensitive = $target_db =~ /SQLServer/;
e12ca55a 165
942485ea 166my $s1_name = $source_schema->name;
167my $s2_name = $target_schema->name;
d71512a6 168my ( @new_tables, @diffs , @diffs_at_end);
942485ea 169for my $t1 ( $source_schema->get_tables ) {
170 my $t1_name = $t1->name;
d71512a6 171 my $t2 = $target_schema->get_table( $t1_name, $case_insensitive );
942485ea 172
8b3b2f0c 173 warn "TABLE '$s1_name.$t1_name'\n" if $debug;
942485ea 174 unless ( $t2 ) {
8b3b2f0c 175 warn "Couldn't find table '$s1_name.$t1_name' in '$s2_name'\n"
176 if $debug;
d71512a6 177 if ( $target_db =~ /SQLServer/ ) {
178 for my $constraint ( $t1->get_constraints ) {
179 next if $constraint->type eq PRIMARY_KEY;
180 push @diffs_at_end, "ALTER TABLE $t1_name ADD ".
181 constraint_to_string($constraint, $source_schema).";";
182 $t1->drop_constraint($constraint);
183 }
184 }
942485ea 185 push @new_tables, $t1;
186 next;
187 }
d71512a6 188
189 # Go through our options
190 my $options_different = 0;
191 my %checkedOptions;
192OPTION:
193 for my $t1_option_ref ( $t1->options ) {
194 my($key1, $value1) = %{$t1_option_ref};
195 for my $t2_option_ref ( $t2->options ) {
196 my($key2, $value2) = %{$t2_option_ref};
197 if ( $key1 eq $key2 ) {
198 if ( defined $value1 != defined $value2 ) {
199 $options_different = 1;
200 last OPTION;
201 }
202 if ( defined $value1 && $value1 ne $value2 ) {
203 $options_different = 1;
204 last OPTION;
205 }
206 $checkedOptions{$key1} = 1;
207 next OPTION;
208 }
209 }
210 $options_different = 1;
211 last OPTION;
212 }
213 # Go through the other table's options
214 unless ( $options_different ) {
215 for my $t2_option_ref ( $t2->options ) {
216 my($key, $value) = %{$t2_option_ref};
217 next if $checkedOptions{$key};
218 $options_different = 1;
219 last;
220 }
221 }
222 # If there's a difference, just re-set all the options
223 my @diffs_table_options;
224 if ( $options_different ) {
225 my @options = ();
226 foreach my $option_ref ( $t1->options ) {
227 my($key, $value) = %{$option_ref};
228 push(@options, defined $value ? "$key=$value" : $key);
229 }
230 my $options = join(' ', @options);
231 @diffs_table_options = ("ALTER TABLE $t1_name $options;");
232 }
233
942485ea 234 my $t2_name = $t2->name;
d71512a6 235 my(@diffs_table_adds, @diffs_table_changes);
942485ea 236 for my $t1_field ( $t1->get_fields ) {
afdf6a1c 237 my $f1_type = $t1_field->data_type;
238 my $f1_size = $t1_field->size;
239 my $f1_name = $t1_field->name;
d71512a6 240 my $f1_nullable = $t1_field->is_nullable;
241 my $f1_default = $t1_field->default_value;
242 my $f1_auto_inc = $t1_field->is_auto_increment;
afdf6a1c 243 my $t2_field = $t2->get_field( $f1_name );
942485ea 244 my $f1_full_name = "$s1_name.$t1_name.$t1_name";
8b3b2f0c 245 warn "FIELD '$f1_full_name'\n" if $debug;
e12ca55a 246
afdf6a1c 247 my $f2_full_name = "$s2_name.$t2_name.$f1_name";
e12ca55a 248
942485ea 249 unless ( $t2_field ) {
8b3b2f0c 250 warn "Couldn't find field '$f2_full_name' in '$t2_name'\n"
251 if $debug;
d71512a6 252 my $temp_default_value = 0;
253 if ( $target_db =~ /SQLServer/ && !$f1_nullable && !defined $f1_default ) {
254 # SQL Server doesn't allow adding non-nullable, non-default columns
255 # so we add it with a default value, then remove the default value
256 $temp_default_value = 1;
257 my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint);
258 $f1_default = grep($_ eq $f1_type, @numeric_types) ? 0 : '';
259 }
260 push @diffs_table_adds, sprintf( "ALTER TABLE %s ADD %s %s%s%s%s%s;",
afdf6a1c 261 $t1_name, $f1_name, $f1_type,
d71512a6 262 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
263 $f1_nullable ? '' : ' NOT NULL',
264 !defined $f1_default ? ''
265 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
266 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
267 : " DEFAULT '$f1_default'",
268 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
afdf6a1c 269 );
d71512a6 270 if ( $temp_default_value ) {
271 undef $f1_default;
568bf60a 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,
d71512a6 289 );
290 }
e12ca55a 291 next;
292 }
293
afdf6a1c 294 my $f2_type = $t2_field->data_type;
d71512a6 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;
d71512a6 299 if ( !$t1_field->equals($t2_field, $case_insensitive) ) {
568bf60a 300 # SQLServer timestamp fields can't be altered, so we drop and add instead
d71512a6 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;",
304 $t1_name, $f1_name, $f1_type,
305 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
306 $f1_nullable ? '' : ' NOT NULL',
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_auto_inc ? ' AUTO_INCREMENT' : '',
312 );
313 next;
314 }
315
316 push @diffs_table_changes, sprintf( "ALTER TABLE %s %s %s%s %s%s%s%s%s;",
317 $t1_name, $target_db =~ /SQLServer/ ? "ALTER COLUMN" : "CHANGE",
318 $f1_name, $target_db =~ /MySQL/ ? " $f1_name" : '',
319 $f1_type, ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
320 $f1_nullable ? '' : ' NOT NULL',
321 !defined $f1_default || $target_db =~ /SQLServer/ ? ''
322 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
323 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
324 : " DEFAULT '$f1_default'",
325 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
afdf6a1c 326 );
d71512a6 327 if ( defined $f1_default && $target_db =~ /SQLServer/ ) {
328 # Adding a column with a default value for SQL Server means adding a
329 # constraint and setting existing NULLs to the default value
330 push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;",
331 $t1_name, $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'DEFAULT NULL'
332 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP'
333 : "DEFAULT '$f1_default'", $f1_name,
334 );
335 push @diffs_table_changes, sprintf( "UPDATE %s SET %s = %s WHERE %s IS NULL;",
336 $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'NULL'
337 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP'
338 : "'$f1_default'", $f1_name,
339 );
e12ca55a 340 }
341 }
342 }
d71512a6 343
568bf60a 344 my(%checked_indices, @diffs_index_creates, @diffs_index_drops);
d71512a6 345INDEX:
346 for my $i1 ( $t1->get_indices ) {
568bf60a 347 for my $i2 ( $t2->get_indices ) {
d71512a6 348 if ( $i1->equals($i2, $case_insensitive) ) {
568bf60a 349 $checked_indices{$i2} = 1;
d71512a6 350 next INDEX;
351 }
352 }
353 push @diffs_index_creates, sprintf(
942485ea 354 "CREATE %sINDEX%s ON %s (%s);",
d71512a6 355 $i1->type eq NORMAL ? '' : $i1->type." ",
356 $i1->name ? " ".$i1->name : '',
942485ea 357 $t1_name,
d71512a6 358 join(",", $i1->fields),
942485ea 359 );
d71512a6 360 }
568bf60a 361INDEX2:
362 for my $i2 ( $t2->get_indices ) {
363 next if $checked_indices{$i2};
364 for my $i1 ( $t1->get_indices ) {
365 next INDEX2 if $i2->equals($i1, $case_insensitive);
366 }
d71512a6 367 $target_db =~ /SQLServer/
368 ? push @diffs_index_drops, "DROP INDEX $t1_name.".$i2->name.";"
369 : push @diffs_index_drops, "DROP INDEX ".$i2->name." on $t1_name;";
370 }
371
568bf60a 372 my(%checked_constraints, @diffs_constraint_adds, @diffs_constraint_drops);
d71512a6 373CONSTRAINT:
374 for my $c1 ( $t1->get_constraints ) {
568bf60a 375 for my $c2 ( $t2->get_constraints ) {
d71512a6 376 if ( $c1->equals($c2, $case_insensitive) ) {
568bf60a 377 $checked_constraints{$c2} = 1;
d71512a6 378 next CONSTRAINT;
379 }
380 }
381 push @diffs_constraint_adds, "ALTER TABLE $t1_name ADD ".
382 constraint_to_string($c1, $source_schema).";";
383 }
568bf60a 384CONSTRAINT2:
385 for my $c2 ( $t2->get_constraints ) {
386 next if $checked_constraints{$c2};
387 for my $c1 ( $t1->get_constraints ) {
388 next CONSTRAINT2 if $c2->equals($c1, $case_insensitive);
389 }
d71512a6 390 if ( $c2->type eq UNIQUE ) {
391 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP INDEX ".
392 $c2->name.";";
393 } elsif ( $target_db =~ /SQLServer/ ) {
394 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->name.";";
395 } else {
396 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->type.
397 ($c2->type eq FOREIGN_KEY ? " ".$c2->name : '').";";
398 }
399 }
400
401 push @diffs, @diffs_index_drops, @diffs_constraint_drops,
402 @diffs_table_options, @diffs_table_adds, @diffs_table_changes,
403 @diffs_constraint_adds, @diffs_index_creates;
942485ea 404}
405
406for my $t2 ( $target_schema->get_tables ) {
407 my $t2_name = $t2->name;
d71512a6 408 my $t1 = $source_schema->get_table( $t2_name, $target_db =~ /SQLServer/ );
942485ea 409
410 unless ( $t1 ) {
d71512a6 411 if ( $target_db =~ /SQLServer/ ) {
412 for my $constraint ( $t2->get_constraints ) {
413 next if $constraint->type eq PRIMARY_KEY;
414 push @diffs, "ALTER TABLE $t2_name DROP ".$constraint->name.";";
415 }
416 }
417 push @diffs_at_end, "DROP TABLE $t2_name;";
942485ea 418 next;
419 }
420
421 for my $t2_field ( $t2->get_fields ) {
422 my $f2_name = $t2_field->name;
423 my $t1_field = $t1->get_field( $f2_name );
424 unless ( $t1_field ) {
d71512a6 425 my $modifier = $target_db =~ /SQLServer/ ? "COLUMN " : '';
426 push @diffs, "ALTER TABLE $t2_name DROP $modifier$f2_name;";
942485ea 427 }
428 }
429}
430
431if ( @new_tables ) {
432 my $dummy_tr = SQL::Translator->new;
433 $dummy_tr->schema->add_table( $_ ) for @new_tables;
434 my $producer = $dummy_tr->producer( $target_db );
435 unshift @diffs, $producer->( $dummy_tr );
e12ca55a 436}
d71512a6 437push(@diffs, @diffs_at_end);
e12ca55a 438
439if ( @diffs ) {
942485ea 440 print join( "\n",
8b3b2f0c 441 "-- Convert schema '$s2_name' to '$s1_name':\n", @diffs, ''
942485ea 442 );
e12ca55a 443}
444else {
445 print "There were no differences.\n";
446}
447
d71512a6 448sub constraint_to_string {
449 my $c = shift;
450 my $schema = shift or die "No schema given";
451 my @fields = $c->fields or return '';
452
453 if ( $c->type eq PRIMARY_KEY ) {
454 return 'PRIMARY KEY (' . join(', ', @fields). ')';
455 }
456 elsif ( $c->type eq UNIQUE ) {
457 return 'UNIQUE '.
458 (defined $c->name ? $c->name.' ' : '').
459 '(' . join(', ', @fields). ')';
460 }
461 elsif ( $c->type eq FOREIGN_KEY ) {
462 my $def = join(' ',
463 map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY'
464 );
465
466 $def .= ' (' . join( ', ', @fields ) . ')';
467
468 $def .= ' REFERENCES ' . $c->reference_table;
469
470 my @rfields = map { $_ || () } $c->reference_fields;
471 unless ( @rfields ) {
472 my $rtable_name = $c->reference_table;
473 if ( my $ref_table = $schema->get_table( $rtable_name ) ) {
474 push @rfields, $ref_table->primary_key;
475 }
476 else {
477 warn "Can't find reference table '$rtable_name' " .
478 "in schema\n";
479 }
480 }
481
482 if ( @rfields ) {
483 $def .= ' (' . join( ', ', @rfields ) . ')';
484 }
485 else {
486 warn "FK constraint on " . 'some table' . '.' .
487 join('', @fields) . " has no reference fields\n";
488 }
489
490 if ( $c->match_type ) {
491 $def .= ' MATCH ' .
492 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
493 }
494
495 if ( $c->on_delete ) {
496 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
497 }
498
499 if ( $c->on_update ) {
500 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
501 }
502
503 return $def;
504 }
505}
506
e12ca55a 507# -------------------------------------------------------------------
508# Bring out number weight & measure in a year of dearth.
509# William Blake
510# -------------------------------------------------------------------
511
512=pod
513
514=head1 AUTHOR
515
516Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
517
518=head1 SEE ALSO
519
520SQL::Translator, L<http://sqlfairy.sourceforge.net>.
521
522=cut