Factor out calling of normal diff-production functions
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Utils.pm
CommitLineData
1a24938d 1package SQL::Translator::Utils;
2
1a24938d 3use strict;
f27f9229 4use warnings;
c092c5b3 5use Digest::SHA qw( sha1_hex );
bdf60588 6use File::Spec;
45287c81 7use Scalar::Util qw(blessed);
8use Try::Tiny;
f8783818 9use Carp qw(carp);
1a24938d 10
0c04c5a2 11our $VERSION = '1.59';
12our $DEFAULT_COMMENT = '-- ';
bdf60588 13
14use base qw(Exporter);
0c04c5a2 15our @EXPORT_OK = qw(
7b4b17aa 16 debug normalize_name header_comment parse_list_arg truncate_id_uniquely
17 $DEFAULT_COMMENT parse_mysql_version parse_dbms_version
86609eaa 18 ddl_parser_instance batch_alter_table_statements
f8783818 19 throw ex2err carp_ro
118bb73f 20);
11ad2df9 21use constant COLLISION_TAG_LENGTH => 8;
1a24938d 22
1a24938d 23sub debug {
a2ba36ba 24 my ($pkg, $file, $line, $sub) = caller(0);
1a24938d 25 {
26 no strict qw(refs);
27 return unless ${"$pkg\::DEBUG"};
28 }
29
30 $sub =~ s/^$pkg\:://;
31
32 while (@_) {
33 my $x = shift;
34 chomp $x;
35 $x =~ s/\bPKG\b/$pkg/g;
36 $x =~ s/\bLINE\b/$line/g;
37 $x =~ s/\bSUB\b/$sub/g;
38 #warn '[' . $x . "]\n";
39 print STDERR '[' . $x . "]\n";
40 }
41}
42
93d12e9c 43sub normalize_name {
ae48473b 44 my $name = shift or return '';
93d12e9c 45
46 # The name can only begin with a-zA-Z_; if there's anything
47 # else, prefix with _
48 $name =~ s/^([^a-zA-Z_])/_$1/;
49
50 # anything other than a-zA-Z0-9_ in the non-first position
51 # needs to be turned into _
52 $name =~ tr/[a-zA-Z0-9_]/_/c;
53
54 # All duplicated _ need to be squashed into one.
55 $name =~ tr/_/_/s;
56
57 # Trim a trailing _
58 $name =~ s/_$//;
59
60 return $name;
61}
62
a2ba36ba 63sub header_comment {
64 my $producer = shift || caller;
65 my $comment_char = shift;
66 my $now = scalar localtime;
67
68 $comment_char = $DEFAULT_COMMENT
69 unless defined $comment_char;
70
71 my $header_comment =<<"HEADER_COMMENT";
72${comment_char}
73${comment_char}Created by $producer
74${comment_char}Created on $now
75${comment_char}
76HEADER_COMMENT
77
78 # Any additional stuff passed in
79 for my $additional_comment (@_) {
80 $header_comment .= "${comment_char}${additional_comment}\n";
81 }
82
83 return $header_comment;
84}
85
e545d971 86sub parse_list_arg {
87 my $list = UNIVERSAL::isa( $_[0], 'ARRAY' ) ? shift : [ @_ ];
88
51bb6fe0 89 #
90 # This protects stringification of references.
91 #
92 if ( @$list && ref $list->[0] ) {
93 return $list;
94 }
95 #
96 # This processes string-like arguments.
97 #
98 else {
ea93df61 99 return [
51bb6fe0 100 map { s/^\s+|\s+$//g; $_ }
101 map { split /,/ }
102 grep { defined && length } @$list
103 ];
104 }
118bb73f 105}
106
f5405d47 107sub truncate_id_uniquely {
108 my ( $desired_name, $max_symbol_length ) = @_;
109
16fa91c0 110 return $desired_name
111 unless defined $desired_name && length $desired_name > $max_symbol_length;
f5405d47 112
16fa91c0 113 my $truncated_name = substr $desired_name, 0,
11ad2df9 114 $max_symbol_length - COLLISION_TAG_LENGTH - 1;
f5405d47 115
116 # Hex isn't the most space-efficient, but it skirts around allowed
117 # charset issues
118 my $digest = sha1_hex($desired_name);
11ad2df9 119 my $collision_tag = substr $digest, 0, COLLISION_TAG_LENGTH;
f5405d47 120
121 return $truncated_name
122 . '_'
123 . $collision_tag;
124}
125
5d666b31 126
5d666b31 127sub parse_mysql_version {
128 my ($v, $target) = @_;
129
130 return undef unless $v;
131
132 $target ||= 'perl';
133
134 my @vers;
135
ea93df61 136 # X.Y.Z style
5d666b31 137 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
138 push @vers, $1, $2, $3;
139 }
140
ea93df61 141 # XYYZZ (mysql) style
5d666b31 142 elsif ( $v =~ / ^ (\d) (\d{2}) (\d{2}) $ /x ) {
143 push @vers, $1, $2, $3;
144 }
145
ea93df61 146 # XX.YYYZZZ (perl) style or simply X
5d666b31 147 elsif ( $v =~ / ^ (\d+) (?: \. (\d{3}) (\d{3}) )? $ /x ) {
148 push @vers, $1, $2, $3;
149 }
150 else {
151 #how do I croak sanely here?
152 die "Unparseable MySQL version '$v'";
153 }
154
155 if ($target eq 'perl') {
156 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
157 }
158 elsif ($target eq 'mysql') {
159 return sprintf ('%d%02d%02d', map { $_ || 0 } (@vers) );
160 }
161 else {
162 #how do I croak sanely here?
163 die "Unknown version target '$target'";
164 }
165}
166
7b4b17aa 167sub parse_dbms_version {
168 my ($v, $target) = @_;
169
170 return undef unless $v;
171
172 my @vers;
173
ea93df61 174 # X.Y.Z style
7b4b17aa 175 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
176 push @vers, $1, $2, $3;
177 }
178
ea93df61 179 # XX.YYYZZZ (perl) style or simply X
7b4b17aa 180 elsif ( $v =~ / ^ (\d+) (?: \. (\d{3}) (\d{3}) )? $ /x ) {
181 push @vers, $1, $2, $3;
182 }
183 else {
184 #how do I croak sanely here?
185 die "Unparseable database server version '$v'";
186 }
187
188 if ($target eq 'perl') {
189 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
190 }
191 elsif ($target eq 'native') {
e0d18105 192 return join '.' => grep defined, @vers;
7b4b17aa 193 }
194 else {
195 #how do I croak sanely here?
196 die "Unknown version target '$target'";
197 }
198}
5d666b31 199
0eb3b94a 200#my ($parsers_libdir, $checkout_dir);
bdf60588 201sub ddl_parser_instance {
0eb3b94a 202
bdf60588 203 my $type = shift;
204
205 # it may differ from our caller, even though currently this is not the case
206 eval "require SQL::Translator::Parser::$type"
207 or die "Unable to load grammar-spec container SQL::Translator::Parser::$type:\n$@";
208
7e666ece 209 # handle DB2 in a special way, since the grammar source was lost :(
210 if ($type eq 'DB2') {
211 require SQL::Translator::Parser::DB2::Grammar;
212 return SQL::Translator::Parser::DB2::Grammar->new;
213 }
214
0eb3b94a 215 require Parse::RecDescent;
216 return Parse::RecDescent->new(do {
217 no strict 'refs';
218 ${"SQL::Translator::Parser::${type}::GRAMMAR"}
219 || die "No \$SQL::Translator::Parser::${type}::GRAMMAR defined, unable to instantiate PRD parser\n"
220 });
221
222# this is disabled until RT#74593 is resolved
1007dce2 223
224=begin sadness
0eb3b94a 225
bdf60588 226 unless ($parsers_libdir) {
227
228 # are we in a checkout?
229 if ($checkout_dir = _find_co_root()) {
230 $parsers_libdir = File::Spec->catdir($checkout_dir, 'share', 'PrecompiledParsers');
231 }
232 else {
233 require File::ShareDir;
234 $parsers_libdir = File::Spec->catdir(
235 File::ShareDir::dist_dir('SQL-Translator'),
236 'PrecompiledParsers'
237 );
238 }
239
240 unshift @INC, $parsers_libdir;
241 }
242
243 my $precompiled_mod = "Parse::RecDescent::DDL::SQLT::$type";
244
245 # FIXME FIXME FIXME
246 # Parse::RecDescent has horrible architecture where each precompiled parser
247 # instance shares global state with all its siblings
248 # What we do here is gross, but scarily efficient - the parser compilation
249 # is much much slower than an unload/reload cycle
f2ab5843 250 require Class::Unload;
bdf60588 251 Class::Unload->unload($precompiled_mod);
252
253 # There is also a sub-namespace that P::RD uses, but simply unsetting
254 # $^W to stop redefine warnings seems to be enough
255 #Class::Unload->unload("Parse::RecDescent::$precompiled_mod");
256
257 eval "local \$^W; require $precompiled_mod" or do {
258 if ($checkout_dir) {
259 die "Unable to find precompiled grammar for $type - run Makefile.PL to generate it\n";
260 }
261 else {
262 die "Unable to load precompiled grammar for $type... this is not supposed to happen if you are not in a checkout, please file a bugreport:\n$@"
263 }
264 };
265
266 my $grammar_spec_fn = $INC{"SQL/Translator/Parser/$type.pm"};
267 my $precompiled_fn = $INC{"Parse/RecDescent/DDL/SQLT/$type.pm"};
268
269 if (
270 (stat($grammar_spec_fn))[9]
271 >
272 (stat($precompiled_fn))[9]
273 ) {
274 die (
275 "Grammar spec '$grammar_spec_fn' is newer than precompiled parser '$precompiled_fn'"
276 . ($checkout_dir
277 ? " - run Makefile.PL to regenerate stale versions\n"
278 : "... this is not supposed to happen if you are not in a checkout, please file a bugreport\n"
279 )
280 );
281 }
282
283 return $precompiled_mod->new;
1007dce2 284
285=end sadness
286
0eb3b94a 287=cut
288
bdf60588 289}
290
291# Try to determine the root of a checkout/untar if possible
292# or return undef
293sub _find_co_root {
294
295 my @mod_parts = split /::/, (__PACKAGE__ . '.pm');
296 my $rel_path = join ('/', @mod_parts); # %INC stores paths with / regardless of OS
297
298 return undef unless ($INC{$rel_path});
299
300 # a bit convoluted, but what we do here essentially is:
301 # - get the file name of this particular module
302 # - do 'cd ..' as many times as necessary to get to lib/SQL/Translator/../../..
303
304 my $root = (File::Spec::Unix->splitpath($INC{$rel_path}))[1];
305 for (1 .. @mod_parts) {
306 $root = File::Spec->catdir($root, File::Spec->updir);
307 }
308
309 return ( -f File::Spec->catfile($root, 'Makefile.PL') )
310 ? $root
311 : undef
312 ;
313}
314
45287c81 315{
316 package SQL::Translator::Utils::Error;
317
318 use overload
319 '""' => sub { ${$_[0]} },
320 fallback => 1;
321
322 sub new {
323 my ($class, $msg) = @_;
324 bless \$msg, $class;
325 }
326}
327
328sub throw {
329 die SQL::Translator::Utils::Error->new($_[0]);
330}
331
332sub ex2err {
333 my ($orig, $self, @args) = @_;
334 return try {
335 $self->$orig(@args);
336 } catch {
337 die $_ unless blessed($_) && $_->isa("SQL::Translator::Utils::Error");
338 $self->error("$_");
339 };
340}
341
f8783818 342sub carp_ro {
343 my ($name) = @_;
344 return sub {
345 my ($orig, $self) = (shift, shift);
346 carp "'$name' is a read-only accessor" if @_;
347 return $self->$orig;
348 };
349}
350
86609eaa 351sub batch_alter_table_statements {
352 my ($diff_hash, $options, @meths) = @_;
353
354 @meths = qw(
355 rename_table
356 alter_drop_constraint
357 alter_drop_index
358 drop_field
359 add_field
360 alter_field
361 rename_field
362 alter_create_index
363 alter_create_constraint
364 alter_table
365 ) unless @meths;
366
367 my $package = caller;
368
369 return map {
370 my $meth = $package->can($_) or die "$package cant $_";
371 map { $meth->(ref $_ eq 'ARRAY' ? @$_ : $_, $options) } @{ $diff_hash->{$_} }
372 } grep { @{$diff_hash->{$_} || []} }
373 @meths;
374}
375
1a24938d 3761;
377
118bb73f 378=pod
1a24938d 379
380=head1 NAME
381
382SQL::Translator::Utils - SQL::Translator Utility functions
383
384=head1 SYNOPSIS
385
386 use SQL::Translator::Utils qw(debug);
387 debug("PKG: Bad things happened");
388
389=head1 DESCSIPTION
390
391C<SQL::Translator::Utils> contains utility functions designed to be
392used from the other modules within the C<SQL::Translator> modules.
393
a2ba36ba 394Nothing is exported by default.
1a24938d 395
a2ba36ba 396=head1 EXPORTED FUNCTIONS AND CONSTANTS
1a24938d 397
398=head2 debug
399
400C<debug> takes 0 or more messages, which will be sent to STDERR using
401C<warn>. Occurances of the strings I<PKG>, I<SUB>, and I<LINE>
402will be replaced by the calling package, subroutine, and line number,
e545d971 403respectively, as reported by C<caller(1)>.
1a24938d 404
405For example, from within C<foo> in F<SQL/Translator.pm>, at line 666:
406
407 debug("PKG: Error reading file at SUB/LINE");
408
409Will warn
410
411 [SQL::Translator: Error reading file at foo/666]
412
413The entire message is enclosed within C<[> and C<]> for visual clarity
414when STDERR is intermixed with STDOUT.
93d12e9c 415
416=head2 normalize_name
417
418C<normalize_name> takes a string and ensures that it is suitable for
419use as an identifier. This means: ensure that it starts with a letter
420or underscore, and that the rest of the string consists of only
421letters, numbers, and underscores. A string that begins with
422something other than [a-zA-Z] will be prefixer with an underscore, and
423all other characters in the string will be replaced with underscores.
424Finally, a trailing underscore will be removed, because that's ugly.
425
426 normalize_name("Hello, world");
427
428Produces:
429
430 Hello_world
431
432A more useful example, from the C<SQL::Translator::Parser::Excel> test
433suite:
434
435 normalize_name("silly field (with random characters)");
436
437returns:
438
439 silly_field_with_random_characters
440
a2ba36ba 441=head2 header_comment
442
443Create the header comment. Takes 1 mandatory argument (the producer
444classname), an optional comment character (defaults to $DEFAULT_COMMENT),
445and 0 or more additional comments, which will be appended to the header,
446prefixed with the comment character. If additional comments are provided,
447then a comment string must be provided ($DEFAULT_COMMENT is exported for
448this use). For example, this:
449
450 package My::Producer;
451
452 use SQL::Translator::Utils qw(header_comment $DEFAULT_COMMENT);
453
454 print header_comment(__PACKAGE__,
e545d971 455 $DEFAULT_COMMENT,
a2ba36ba 456 "Hi mom!");
457
458produces:
459
e545d971 460 --
a2ba36ba 461 -- Created by My::Prodcuer
462 -- Created on Fri Apr 25 06:56:02 2003
e545d971 463 --
a2ba36ba 464 -- Hi mom!
e545d971 465 --
a2ba36ba 466
467Note the gratuitous spacing.
468
118bb73f 469=head2 parse_list_arg
470
471Takes a string, list or arrayref (all of which could contain
472comma-separated values) and returns an array reference of the values.
473All of the following will return equivalent values:
474
475 parse_list_arg('id');
476 parse_list_arg('id', 'name');
477 parse_list_arg( 'id, name' );
478 parse_list_arg( [ 'id', 'name' ] );
479 parse_list_arg( qw[ id name ] );
480
f5405d47 481=head2 truncate_id_uniquely
482
483Takes a string ($desired_name) and int ($max_symbol_length). Truncates
484$desired_name to $max_symbol_length by including part of the hash of
485the full name at the end of the truncated name, giving a high
486probability that the symbol will be unique. For example,
487
488 truncate_id_uniquely( 'a' x 100, 64 )
489 truncate_id_uniquely( 'a' x 99 . 'b', 64 );
490 truncate_id_uniquely( 'a' x 99, 64 )
491
492Will give three different results; specifically:
493
494 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7f900025
495 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_6191e39a
496 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_8cd96af2
497
a2ba36ba 498=head2 $DEFAULT_COMMENT
499
500This is the default comment string, '-- ' by default. Useful for
501C<header_comment>.
502
5d666b31 503=head2 parse_mysql_version
504
ea93df61 505Used by both L<Parser::MySQL|SQL::Translator::Parser::MySQL> and
5d666b31 506L<Producer::MySQL|SQL::Translator::Producer::MySQL> in order to provide a
507consistent format for both C<< parser_args->{mysql_parser_version} >> and
508C<< producer_args->{mysql_version} >> respectively. Takes any of the following
509version specifications:
510
511 5.0.3
512 4.1
513 3.23.2
514 5
515 5.001005 (perl style)
516 30201 (mysql style)
517
282bf498 518=head2 parse_dbms_version
519
520Takes a version string (X.Y.Z) or perl style (XX.YYYZZZ) and a target ('perl'
521or 'native') transforms the string to the given target style.
522to
523
4e43db0d 524=head2 throw
525
526Throws the provided string as an object that will stringify back to the
527original string. This stops it from being mangled by L<Moo>'s C<isa>
528code.
529
530=head2 ex2err
531
532Wraps an attribute accessor to catch any exception raised using
533L</throw> and store them in C<< $self->error() >>, finally returning
534undef. A reference to this function can be passed directly to
535L<Moo/around>.
536
537 around foo => \&ex2err;
538
539 around bar => sub {
540 my ($orig, $self) = (shift, shift);
541 return ex2err($orig, $self, @_) if @_;
542 ...
543 };
544
f8783818 545=head2 carp_ro
546
547Takes a field name and returns a reference to a function can be used
548L<around|Moo/around> a read-only accessor to make it L<carp|Carp/carp>
549instead of die when passed an argument.
550
86609eaa 551=head2 batch_alter_table_statements
552
553Takes diff and argument hashes as passed to
554L<batch_alter_table|SQL::Translator::Diff/batch_alter_table($table, $hash) (optional)>
555and an optional list of producer functions to call on the calling package.
556Returns the list of statements returned by the producer functions.
557
558If no producer functions are specified, the following functions in the
559calling package are called:
560
561=over
562
563=item 1. rename_table
564
565=item 2. alter_drop_constraint
566
567=item 3. alter_drop_index
568
569=item 4. drop_field
570
571=item 5. add_field
572
573=item 5. alter_field
574
575=item 6. rename_field
576
577=item 7. alter_create_index
578
579=item 8. alter_create_constraint
580
581=item 9. alter_table
582
583=back
584
585If the corresponding array in the hash has any elements, but the
586caller doesn't implement that function, an exception is thrown.
587
118bb73f 588=head1 AUTHORS
589
590Darren Chamberlain E<lt>darren@cpan.orgE<gt>,
11ad2df9 591Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
118bb73f 592
593=cut