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 80     80   205227 use strict;
  80         195  
  80         2297  
4 80     80   420 use warnings;
  80         160  
  80         2344  
5 80     80   43046 use Digest::SHA qw( sha1_hex );
  80         240003  
  80         6465  
6 80     80   708 use File::Spec;
  80         183  
  80         2394  
7 80     80   477 use Scalar::Util qw(blessed);
  80         162  
  80         3842  
8 80     80   41627 use Try::Tiny;
  80         117309  
  80         4699  
9 80     80   572 use Carp qw(carp croak);
  80         195  
  80         4742  
10              
11             our $VERSION = '1.62';
12              
13 80     80   503 use base qw(Exporter);
  80         188  
  80         11091  
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 80     80   600 use constant COLLISION_TAG_LENGTH => 8;
  80         201  
  80         7983  
22              
23             our $DEFAULT_COMMENT = '--';
24              
25             sub debug {
26 2355     2355 1 9984 my ($pkg, $file, $line, $sub) = caller(0);
27             {
28 80     80   591 no strict qw(refs);
  80         196  
  80         119137  
  2355         48328  
29 2355 50       3419 return unless ${"$pkg\::DEBUG"};
  2355         17056  
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 3733 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         69 $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         49 $name =~ tr/[a-zA-Z0-9_]/_/c;
55              
56             # All duplicated _ need to be squashed into one.
57 21         36 $name =~ tr/_/_/s;
58              
59             # Trim a trailing _
60 21         47 $name =~ s/_$//;
61              
62 21         65 return $name;
63             }
64              
65             sub normalize_quote_options {
66 577     577 0 1057 my $config = shift;
67              
68 577         942 my $quote;
69 577 100 75     3391 if (defined $config->{quote_identifiers}) {
    100          
    100          
70 226         390 $quote = $config->{quote_identifiers};
71              
72 226         460 for (qw/quote_table_names quote_field_names/) {
73             carp "Ignoring deprecated parameter '$_', since 'quote_identifiers' is supplied"
74 452 50       1116 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       30 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       23 unless $config->{'quote_table_names'};
86 9 50       24 $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 113 100 75     477 if ($config->{'quote_table_names'} xor $config->{'quote_field_names'});
98              
99 112 100       255 $quote = $config->{'quote_table_names'} ? 1 : 0;
100             }
101              
102 576         3774 return $quote;
103             }
104              
105             sub header_comment {
106 16   66 16 1 1577 my $producer = shift || caller;
107 16         280 my $comment_char = shift;
108 16         1040 my $now = scalar localtime;
109              
110 16 100       109 $comment_char = $DEFAULT_COMMENT
111             unless defined $comment_char;
112              
113 16         102 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 16         57 for my $additional_comment (@_) {
122 1         4 $header_comment .= "${comment_char} ${additional_comment}\n";
123             }
124              
125 16         101 return $header_comment;
126             }
127              
128             sub parse_list_arg {
129 12857 100   12857 1 81602 my $list = UNIVERSAL::isa( $_[0], 'ARRAY' ) ? shift : [ @_ ];
130              
131             #
132             # This protects stringification of references.
133             #
134 12857 100 100     44850 if ( @$list && ref $list->[0] ) {
135 431         3031 return $list;
136             }
137             #
138             # This processes string-like arguments.
139             #
140             else {
141             return [
142 3005         9840 map { s/^\s+|\s+$//g; $_ }
  3005         15632  
143 2958         10643 map { split /,/ }
144 12426 100       48679 grep { defined && length } @$list
  3155         16061  
145             ];
146             }
147             }
148              
149             sub truncate_id_uniquely {
150 130     130 1 3206 my ( $desired_name, $max_symbol_length ) = @_;
151              
152 130 100 66     809 return $desired_name
153             unless defined $desired_name && length $desired_name > $max_symbol_length;
154              
155 15         68 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 15         118 my $digest = sha1_hex($desired_name);
161 15         37 my $collision_tag = substr $digest, 0, COLLISION_TAG_LENGTH;
162              
163 15         65 return $truncated_name
164             . '_'
165             . $collision_tag;
166             }
167              
168              
169             sub parse_mysql_version {
170 66     66 1 7812 my ($v, $target) = @_;
171              
172 66 100       568 return undef unless $v;
173              
174 15   100     53 $target ||= 'perl';
175              
176 15         31 my @vers;
177              
178             # X.Y.Z style
179 15 100       167 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
    100          
    100          
180 8         41 push @vers, $1, $2, $3;
181             }
182              
183             # XYYZZ (mysql) style
184             elsif ( $v =~ / ^ (\d) (\d{2}) (\d{2}) $ /x ) {
185 4         27 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         10 push @vers, $1, $2, $3;
191             }
192             else {
193             #how do I croak sanely here?
194 1         17 die "Unparseable MySQL version '$v'";
195             }
196              
197 14 100       55 if ($target eq 'perl') {
    50          
198 7 100       18 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
  21         160  
199             }
200             elsif ($target eq 'mysql') {
201 7 100       19 return sprintf ('%d%02d%02d', map { $_ || 0 } (@vers) );
  21         148  
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 16     16 1 67 my ($v, $target) = @_;
211              
212 16 50       88 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 70     70 0 9580 my $type = shift;
246              
247             # it may differ from our caller, even though currently this is not the case
248 70 50       5356 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 70 100       402 if ($type eq 'DB2') {
253 2         8802 require SQL::Translator::Parser::DB2::Grammar;
254 2         21 return SQL::Translator::Parser::DB2::Grammar->new;
255             }
256              
257 68         34894 require Parse::RecDescent;
258 68         997114 return Parse::RecDescent->new(do {
259 80     80   733 no strict 'refs';
  80         212  
  80         24137  
260 68 50       161 ${"SQL::Translator::Parser::${type}::GRAMMAR"}
  68         1432  
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   43 '""' => sub { ${$_[0]} },
  21         479  
362 80     80   15541 fallback => 1;
  80         12056  
  80         996  
363              
364             sub new {
365 21     21   60 my ($class, $msg) = @_;
366 21         458 bless \$msg, $class;
367             }
368             }
369              
370             sub uniq {
371 985     985 0 1877 my( %seen, $seen_undef, $numeric_preserving_copy );
372 985         4715 grep { not (
373             defined $_
374 1055 50       20964 ? $seen{ $numeric_preserving_copy = $_ }++
375             : $seen_undef++
376             ) } @_;
377             }
378              
379             sub throw {
380 21     21 1 268 die SQL::Translator::Utils::Error->new($_[0]);
381             }
382              
383             sub ex2err {
384 54029     54029 1 723201 my ($orig, $self, @args) = @_;
385             return try {
386 54029     54029   3709203 $self->$orig(@args);
387             } catch {
388 22 100 66 22   1084 die $_ unless blessed($_) && $_->isa("SQL::Translator::Utils::Error");
389 21         177 $self->error("$_");
390 54029         290810 };
391             }
392              
393             sub carp_ro {
394 423     423 1 1446 my ($name) = @_;
395             return sub {
396 666     666   76781 my ($orig, $self) = (shift, shift);
397 666 50       2017 carp "'$name' is a read-only accessor" if @_;
398 666         8575 return $self->$orig;
399 423         3446 };
400             }
401              
402             sub batch_alter_table_statements {
403 51     51 1 119 my ($diff_hash, $options, @meths) = @_;
404              
405 51 100       240 @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         156 my $package = caller;
419              
420             return map {
421 75 50       695 my $meth = $package->can($_) or die "$package cant $_";
422 75 100       151 map { $meth->(ref $_ eq 'ARRAY' ? @$_ : $_, $options) } @{ $diff_hash->{$_} }
  110         635  
  75         184  
423 51 100       115 } grep { @{$diff_hash->{$_} || []} }
  488         632  
  488         1213  
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