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