File Coverage

blib/lib/SQL/Translator/Utils.pm
Criterion Covered Total %
statement 133 161 82.6
branch 57 88 64.7
condition 17 22 77.2
subroutine 31 32 96.8
pod 11 14 78.5
total 249 317 78.5


line stmt bran cond sub pod time code
1             package SQL::Translator::Utils;
2              
3 82     82   203999 use strict;
  82         219  
  82         2289  
4 82     82   422 use warnings;
  82         187  
  82         2439  
5 82     82   44020 use Digest::SHA qw( sha1_hex );
  82         246129  
  82         6738  
6 82     82   666 use File::Spec;
  82         183  
  82         2405  
7 82     82   471 use Scalar::Util qw(blessed);
  82         210  
  82         3979  
8 82     82   43256 use Try::Tiny;
  82         119222  
  82         4811  
9 82     82   591 use Carp qw(carp croak);
  82         208  
  82         4663  
10              
11             our $VERSION = '1.63';
12              
13 82     82   539 use base qw(Exporter);
  82         208  
  82         11480  
14             our @EXPORT_OK = qw(
15             debug normalize_name header_comment parse_list_arg truncate_id_uniquely
16             $DEFAULT_COMMENT parse_mysql_version parse_dbms_version
17             ddl_parser_instance batch_alter_table_statements
18             uniq throw ex2err carp_ro
19             normalize_quote_options
20             );
21 82     82   639 use constant COLLISION_TAG_LENGTH => 8;
  82         199  
  82         8183  
22              
23             our $DEFAULT_COMMENT = '--';
24              
25             sub debug {
26 580     580 1 4635 my ($pkg, $file, $line, $sub) = caller(0);
27             {
28 82     82   682 no strict qw(refs);
  82         271  
  82         120294  
  580         12330  
29 580 50       903 return unless ${"$pkg\::DEBUG"};
  580         3226  
30             }
31              
32 0         0 $sub =~ s/^$pkg\:://;
33              
34 0         0 while (@_) {
35 0         0 my $x = shift;
36 0         0 chomp $x;
37 0         0 $x =~ s/\bPKG\b/$pkg/g;
38 0         0 $x =~ s/\bLINE\b/$line/g;
39 0         0 $x =~ s/\bSUB\b/$sub/g;
40             #warn '[' . $x . "]\n";
41 0         0 print STDERR '[' . $x . "]\n";
42             }
43             }
44              
45             sub normalize_name {
46 21 50   21 1 4830 my $name = shift or return '';
47              
48             # The name can only begin with a-zA-Z_; if there's anything
49             # else, prefix with _
50 21         65 $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 21         45 $name =~ tr/[a-zA-Z0-9_]/_/c;
55              
56             # All duplicated _ need to be squashed into one.
57 21         40 $name =~ tr/_/_/s;
58              
59             # Trim a trailing _
60 21         42 $name =~ s/_$//;
61              
62 21         62 return $name;
63             }
64              
65             sub normalize_quote_options {
66 571     571 0 1188 my $config = shift;
67              
68 571         961 my $quote;
69 571 100 75     3162 if (defined $config->{quote_identifiers}) {
    100          
    100          
70 227         379 $quote = $config->{quote_identifiers};
71              
72 227         445 for (qw/quote_table_names quote_field_names/) {
73             carp "Ignoring deprecated parameter '$_', since 'quote_identifiers' is supplied"
74 454 50       1014 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 9 50       32 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 9 50       28 unless $config->{'quote_table_names'};
86 9 50       25 $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 0 0       0 unless $config->{'quote_field_names'};
91 0 0       0 $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 111 100 75     452 if ($config->{'quote_table_names'} xor $config->{'quote_field_names'});
98              
99 110 100       246 $quote = $config->{'quote_table_names'} ? 1 : 0;
100             }
101              
102 570         3877 return $quote;
103             }
104              
105             sub header_comment {
106 15   66 15 1 2110 my $producer = shift || caller;
107 15         258 my $comment_char = shift;
108 15         971 my $now = scalar localtime;
109              
110 15 100       94 $comment_char = $DEFAULT_COMMENT
111             unless defined $comment_char;
112              
113 15         83 my $header_comment =<<"HEADER_COMMENT";
114             ${comment_char}
115             ${comment_char} Created by $producer
116             ${comment_char} Created on $now
117             ${comment_char}
118             HEADER_COMMENT
119              
120             # Any additional stuff passed in
121 15         62 for my $additional_comment (@_) {
122 1         4 $header_comment .= "${comment_char} ${additional_comment}\n";
123             }
124              
125 15         93 return $header_comment;
126             }
127              
128             sub parse_list_arg {
129 12152 100   12152 1 77483 my $list = UNIVERSAL::isa( $_[0], 'ARRAY' ) ? shift : [ @_ ];
130              
131             #
132             # This protects stringification of references.
133             #
134 12152 100 100     42930 if ( @$list && ref $list->[0] ) {
135 333         2563 return $list;
136             }
137             #
138             # This processes string-like arguments.
139             #
140             else {
141             return [
142 2863         9101 map { s/^\s+|\s+$//g; $_ }
  2863         15795  
143 2844         9958 map { split /,/ }
144 11819 100       45998 grep { defined && length } @$list
  3094         15826  
145             ];
146             }
147             }
148              
149             sub truncate_id_uniquely {
150 118     118 1 3232 my ( $desired_name, $max_symbol_length ) = @_;
151              
152 118 100 66     713 return $desired_name
153             unless defined $desired_name && length $desired_name > $max_symbol_length;
154              
155 12         47 my $truncated_name = substr $desired_name, 0,
156             $max_symbol_length - COLLISION_TAG_LENGTH - 1;
157              
158             # Hex isn't the most space-efficient, but it skirts around allowed
159             # charset issues
160 12         95 my $digest = sha1_hex($desired_name);
161 12         39 my $collision_tag = substr $digest, 0, COLLISION_TAG_LENGTH;
162              
163 12         50 return $truncated_name
164             . '_'
165             . $collision_tag;
166             }
167              
168              
169             sub parse_mysql_version {
170 63     63 1 7566 my ($v, $target) = @_;
171              
172 63 100       541 return undef unless $v;
173              
174 13   100     42 $target ||= 'perl';
175              
176 13         40 my @vers;
177              
178             # X.Y.Z style
179 13 100       141 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
    100          
    100          
180 6         28 push @vers, $1, $2, $3;
181             }
182              
183             # XYYZZ (mysql) style
184             elsif ( $v =~ / ^ (\d) (\d{2}) (\d{2}) $ /x ) {
185 4         28 push @vers, $1, $2, $3;
186             }
187              
188             # XX.YYYZZZ (perl) style or simply X
189             elsif ( $v =~ / ^ (\d+) (?: \. (\d{3}) (\d{3}) )? $ /x ) {
190 2         8 push @vers, $1, $2, $3;
191             }
192             else {
193             #how do I croak sanely here?
194 1         15 die "Unparseable MySQL version '$v'";
195             }
196              
197 12 100       47 if ($target eq 'perl') {
    50          
198 5 100       11 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
  15         121  
199             }
200             elsif ($target eq 'mysql') {
201 7 100       19 return sprintf ('%d%02d%02d', map { $_ || 0 } (@vers) );
  21         179  
202             }
203             else {
204             #how do I croak sanely here?
205 0         0 die "Unknown version target '$target'";
206             }
207             }
208              
209             sub parse_dbms_version {
210 13     13 1 59 my ($v, $target) = @_;
211              
212 13 50       92 return undef unless $v;
213              
214 0         0 my @vers;
215              
216             # X.Y.Z style
217 0 0       0 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
    0          
218 0         0 push @vers, $1, $2, $3;
219             }
220              
221             # XX.YYYZZZ (perl) style or simply X
222             elsif ( $v =~ / ^ (\d+) (?: \. (\d{3}) (\d{3}) )? $ /x ) {
223 0         0 push @vers, $1, $2, $3;
224             }
225             else {
226             #how do I croak sanely here?
227 0         0 die "Unparseable database server version '$v'";
228             }
229              
230 0 0       0 if ($target eq 'perl') {
    0          
231 0 0       0 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
  0         0  
232             }
233             elsif ($target eq 'native') {
234 0         0 return join '.' => grep defined, @vers;
235             }
236             else {
237             #how do I croak sanely here?
238 0         0 die "Unknown version target '$target'";
239             }
240             }
241              
242             #my ($parsers_libdir, $checkout_dir);
243             sub ddl_parser_instance {
244              
245 74     74 0 15699 my $type = shift;
246              
247             # it may differ from our caller, even though currently this is not the case
248 74 50       5772 eval "require SQL::Translator::Parser::$type"
249             or die "Unable to load grammar-spec container SQL::Translator::Parser::$type:\n$@";
250              
251             # handle DB2 in a special way, since the grammar source was lost :(
252 74 100       576 if ($type eq 'DB2') {
253 2         10372 require SQL::Translator::Parser::DB2::Grammar;
254 2         21 return SQL::Translator::Parser::DB2::Grammar->new;
255             }
256              
257 72         38687 require Parse::RecDescent;
258 72         1048986 return Parse::RecDescent->new(do {
259 82     82   774 no strict 'refs';
  82         313  
  82         25204  
260 72 50       158 ${"SQL::Translator::Parser::${type}::GRAMMAR"}
  72         1198  
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
265              
266             =begin sadness
267              
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
292             require Class::Unload;
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;
326              
327             =end sadness
328              
329             =cut
330              
331             }
332              
333             # Try to determine the root of a checkout/untar if possible
334             # or return undef
335             sub _find_co_root {
336              
337 0     0   0 my @mod_parts = split /::/, (__PACKAGE__ . '.pm');
338 0         0 my $rel_path = join ('/', @mod_parts); # %INC stores paths with / regardless of OS
339              
340 0 0       0 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 0         0 my $root = (File::Spec::Unix->splitpath($INC{$rel_path}))[1];
347 0         0 for (1 .. @mod_parts) {
348 0         0 $root = File::Spec->catdir($root, File::Spec->updir);
349             }
350              
351 0 0       0 return ( -f File::Spec->catfile($root, 'Makefile.PL') )
352             ? $root
353             : undef
354             ;
355             }
356              
357             {
358             package SQL::Translator::Utils::Error;
359              
360             use overload
361 21     21   41 '""' => sub { ${$_[0]} },
  21         527  
362 82     82   15658 fallback => 1;
  82         12212  
  82         949  
363              
364             sub new {
365 21     21   59 my ($class, $msg) = @_;
366 21         452 bless \$msg, $class;
367             }
368             }
369              
370             sub uniq {
371 865     865 0 1715 my( %seen, $seen_undef, $numeric_preserving_copy );
372 865         4249 grep { not (
373             defined $_
374 924 50       17772 ? $seen{ $numeric_preserving_copy = $_ }++
375             : $seen_undef++
376             ) } @_;
377             }
378              
379             sub throw {
380 21     21 1 298 die SQL::Translator::Utils::Error->new($_[0]);
381             }
382              
383             sub ex2err {
384 51721     51721 1 722593 my ($orig, $self, @args) = @_;
385             return try {
386 51721     51721   3507893 $self->$orig(@args);
387             } catch {
388 22 100 66 22   1182 die $_ unless blessed($_) && $_->isa("SQL::Translator::Utils::Error");
389 21         170 $self->error("$_");
390 51721         272844 };
391             }
392              
393             sub carp_ro {
394 411     411 1 1536 my ($name) = @_;
395             return sub {
396 570     570   75904 my ($orig, $self) = (shift, shift);
397 570 50       1801 carp "'$name' is a read-only accessor" if @_;
398 570         7585 return $self->$orig;
399 411         3194 };
400             }
401              
402             sub batch_alter_table_statements {
403 51     51 1 121 my ($diff_hash, $options, @meths) = @_;
404              
405 51 100       263 @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 51         121 my $package = caller;
419              
420             return map {
421 75 50       743 my $meth = $package->can($_) or die "$package cant $_";
422 75 100       139 map { $meth->(ref $_ eq 'ARRAY' ? @$_ : $_, $options) } @{ $diff_hash->{$_} }
  114         657  
  75         196  
423 51 100       138 } grep { @{$diff_hash->{$_} || []} }
  488         606  
  488         1255  
424             @meths;
425             }
426              
427             1;
428              
429             =pod
430              
431             =head1 NAME
432              
433             SQL::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              
442             C contains utility functions designed to be
443             used from the other modules within the C modules.
444              
445             Nothing is exported by default.
446              
447             =head1 EXPORTED FUNCTIONS AND CONSTANTS
448              
449             =head2 debug
450              
451             C takes 0 or more messages, which will be sent to STDERR using
452             C. Occurances of the strings I, I, and I
453             will be replaced by the calling package, subroutine, and line number,
454             respectively, as reported by C.
455              
456             For example, from within C in F, at line 666:
457              
458             debug("PKG: Error reading file at SUB/LINE");
459              
460             Will warn
461              
462             [SQL::Translator: Error reading file at foo/666]
463              
464             The entire message is enclosed within C<[> and C<]> for visual clarity
465             when STDERR is intermixed with STDOUT.
466              
467             =head2 normalize_name
468              
469             C takes a string and ensures that it is suitable for
470             use as an identifier. This means: ensure that it starts with a letter
471             or underscore, and that the rest of the string consists of only
472             letters, numbers, and underscores. A string that begins with
473             something other than [a-zA-Z] will be prefixer with an underscore, and
474             all other characters in the string will be replaced with underscores.
475             Finally, a trailing underscore will be removed, because that's ugly.
476              
477             normalize_name("Hello, world");
478              
479             Produces:
480              
481             Hello_world
482              
483             A more useful example, from the C test
484             suite:
485              
486             normalize_name("silly field (with random characters)");
487              
488             returns:
489              
490             silly_field_with_random_characters
491              
492             =head2 header_comment
493              
494             Create the header comment. Takes 1 mandatory argument (the producer
495             classname), an optional comment character (defaults to $DEFAULT_COMMENT),
496             and 0 or more additional comments, which will be appended to the header,
497             prefixed with the comment character. If additional comments are provided,
498             then a comment string must be provided ($DEFAULT_COMMENT is exported for
499             this 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__,
506             $DEFAULT_COMMENT,
507             "Hi mom!");
508              
509             produces:
510              
511             --
512             -- Created by My::Prodcuer
513             -- Created on Fri Apr 25 06:56:02 2003
514             --
515             -- Hi mom!
516             --
517              
518             Note the gratuitous spacing.
519              
520             =head2 parse_list_arg
521              
522             Takes a string, list or arrayref (all of which could contain
523             comma-separated values) and returns an array reference of the values.
524             All 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              
532             =head2 truncate_id_uniquely
533              
534             Takes a string ($desired_name) and int ($max_symbol_length). Truncates
535             $desired_name to $max_symbol_length by including part of the hash of
536             the full name at the end of the truncated name, giving a high
537             probability 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              
543             Will give three different results; specifically:
544              
545             aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7f900025
546             aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_6191e39a
547             aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_8cd96af2
548              
549             =head2 $DEFAULT_COMMENT
550              
551             This is the default comment string, '--' by default. Useful for
552             C.
553              
554             =head2 parse_mysql_version
555              
556             Used by both L and
557             L in order to provide a
558             consistent format for both C<< parser_args->{mysql_parser_version} >> and
559             C<< producer_args->{mysql_version} >> respectively. Takes any of the following
560             version 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              
569             =head2 parse_dbms_version
570              
571             Takes a version string (X.Y.Z) or perl style (XX.YYYZZZ) and a target ('perl'
572             or 'native') transforms the string to the given target style.
573             to
574              
575             =head2 throw
576              
577             Throws the provided string as an object that will stringify back to the
578             original string. This stops it from being mangled by L's C
579             code.
580              
581             =head2 ex2err
582              
583             Wraps an attribute accessor to catch any exception raised using
584             L and store them in C<< $self->error() >>, finally returning
585             undef. A reference to this function can be passed directly to
586             L.
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              
596             =head2 carp_ro
597              
598             Takes a field name and returns a reference to a function can be used
599             L a read-only accessor to make it L
600             instead of die when passed an argument.
601              
602             =head2 batch_alter_table_statements
603              
604             Takes diff and argument hashes as passed to
605             L
606             and an optional list of producer functions to call on the calling package.
607             Returns the list of statements returned by the producer functions.
608              
609             If no producer functions are specified, the following functions in the
610             calling 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              
636             If the corresponding array in the hash has any elements, but the
637             caller doesn't implement that function, an exception is thrown.
638              
639             =head1 AUTHORS
640              
641             Darren Chamberlain Edarren@cpan.orgE,
642             Ken Y. Clark Ekclark@cpan.orgE.
643              
644             =cut