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   171638 use strict;
  80         153  
  80         1945  
4 80     80   359 use warnings;
  80         149  
  80         1844  
5 80     80   34298 use Digest::SHA qw( sha1_hex );
  80         200556  
  80         5585  
6 80     80   559 use File::Spec;
  80         150  
  80         1920  
7 80     80   392 use Scalar::Util qw(blessed);
  80         163  
  80         3217  
8 80     80   34587 use Try::Tiny;
  80         97717  
  80         3884  
9 80     80   543 use Carp qw(carp croak);
  80         146  
  80         3783  
10              
11             our $VERSION = '1.6_3';
12              
13 80     80   412 use base qw(Exporter);
  80         179  
  80         9860  
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   497 use constant COLLISION_TAG_LENGTH => 8;
  80         191  
  80         6499  
22              
23             our $DEFAULT_COMMENT = '--';
24              
25             sub debug {
26 2358     2358 1 8230 my ($pkg, $file, $line, $sub) = caller(0);
27             {
28 80     80   509 no strict qw(refs);
  80         153  
  80         98354  
  2358         39802  
29 2358 50       2852 return unless ${"$pkg\::DEBUG"};
  2358         14177  
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 3718 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         55 $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         39 $name =~ tr/[a-zA-Z0-9_]/_/c;
55              
56             # All duplicated _ need to be squashed into one.
57 21         26 $name =~ tr/_/_/s;
58              
59             # Trim a trailing _
60 21         38 $name =~ s/_$//;
61              
62 21         47 return $name;
63             }
64              
65             sub normalize_quote_options {
66 583     583 0 962 my $config = shift;
67              
68 583         845 my $quote;
69 583 100 75     3098 if (defined $config->{quote_identifiers}) {
    100          
    100          
70 228         360 $quote = $config->{quote_identifiers};
71              
72 228         425 for (qw/quote_table_names quote_field_names/) {
73             carp "Ignoring deprecated parameter '$_', since 'quote_identifiers' is supplied"
74 456 50       860 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       35 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       31 $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     383 if ($config->{'quote_table_names'} xor $config->{'quote_field_names'});
98              
99 112 100       215 $quote = $config->{'quote_table_names'} ? 1 : 0;
100             }
101              
102 582         3286 return $quote;
103             }
104              
105             sub header_comment {
106 16   66 16 1 1944 my $producer = shift || caller;
107 16         213 my $comment_char = shift;
108 16         894 my $now = scalar localtime;
109              
110 16 100       95 $comment_char = $DEFAULT_COMMENT
111             unless defined $comment_char;
112              
113 16         79 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         45 for my $additional_comment (@_) {
122 1         4 $header_comment .= "${comment_char} ${additional_comment}\n";
123             }
124              
125 16         85 return $header_comment;
126             }
127              
128             sub parse_list_arg {
129 13028 100   13028 1 69760 my $list = UNIVERSAL::isa( $_[0], 'ARRAY' ) ? shift : [ @_ ];
130              
131             #
132             # This protects stringification of references.
133             #
134 13028 100 100     38732 if ( @$list && ref $list->[0] ) {
135 433         2535 return $list;
136             }
137             #
138             # This processes string-like arguments.
139             #
140             else {
141             return [
142 3043         8323 map { s/^\s+|\s+$//g; $_ }
  3043         13041  
143 2994         9444 map { split /,/ }
144 12595 100       41294 grep { defined && length } @$list
  3200         14437  
145             ];
146             }
147             }
148              
149             sub truncate_id_uniquely {
150 130     130 1 2857 my ( $desired_name, $max_symbol_length ) = @_;
151              
152 130 100 66     842 return $desired_name
153             unless defined $desired_name && length $desired_name > $max_symbol_length;
154              
155 15         65 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         141 my $digest = sha1_hex($desired_name);
161 15         42 my $collision_tag = substr $digest, 0, COLLISION_TAG_LENGTH;
162              
163 15         62 return $truncated_name
164             . '_'
165             . $collision_tag;
166             }
167              
168              
169             sub parse_mysql_version {
170 66     66 1 6650 my ($v, $target) = @_;
171              
172 66 100       494 return undef unless $v;
173              
174 15   100     49 $target ||= 'perl';
175              
176 15         29 my @vers;
177              
178             # X.Y.Z style
179 15 100       143 if ( $v =~ / ^ (\d+) \. (\d{1,3}) (?: \. (\d{1,3}) )? $ /x ) {
    100          
    100          
180 8         30 push @vers, $1, $2, $3;
181             }
182              
183             # XYYZZ (mysql) style
184             elsif ( $v =~ / ^ (\d) (\d{2}) (\d{2}) $ /x ) {
185 4         24 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         11 push @vers, $1, $2, $3;
191             }
192             else {
193             #how do I croak sanely here?
194 1         13 die "Unparseable MySQL version '$v'";
195             }
196              
197 14 100       51 if ($target eq 'perl') {
    50          
198 7 100       13 return sprintf ('%d.%03d%03d', map { $_ || 0 } (@vers) );
  21         144  
199             }
200             elsif ($target eq 'mysql') {
201 7 100       16 return sprintf ('%d%02d%02d', map { $_ || 0 } (@vers) );
  21         143  
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 18     18 1 65 my ($v, $target) = @_;
211              
212 18 50       67 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 72     72 0 12675 my $type = shift;
246              
247             # it may differ from our caller, even though currently this is not the case
248 72 50       5065 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 72 100       391 if ($type eq 'DB2') {
253 2         7647 require SQL::Translator::Parser::DB2::Grammar;
254 2         16 return SQL::Translator::Parser::DB2::Grammar->new;
255             }
256              
257 70         30073 require Parse::RecDescent;
258 70         829227 return Parse::RecDescent->new(do {
259 80     80   623 no strict 'refs';
  80         198  
  80         20317  
260 70 50       153 ${"SQL::Translator::Parser::${type}::GRAMMAR"}
  70         891  
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   47 '""' => sub { ${$_[0]} },
  21         398  
362 80     80   12400 fallback => 1;
  80         10098  
  80         746  
363              
364             sub new {
365 21     21   57 my ($class, $msg) = @_;
366 21         409 bless \$msg, $class;
367             }
368             }
369              
370             sub uniq {
371 1010     1010 0 1771 my( %seen, $seen_undef, $numeric_preserving_copy );
372 1010         4172 grep { not (
373             defined $_
374 1076 50       17592 ? $seen{ $numeric_preserving_copy = $_ }++
375             : $seen_undef++
376             ) } @_;
377             }
378              
379             sub throw {
380 21     21 1 255 die SQL::Translator::Utils::Error->new($_[0]);
381             }
382              
383             sub ex2err {
384 54800     54800 1 624439 my ($orig, $self, @args) = @_;
385             return try {
386 54800     54800   3103951 $self->$orig(@args);
387             } catch {
388 22 100 66 22   1001 die $_ unless blessed($_) && $_->isa("SQL::Translator::Utils::Error");
389 21         135 $self->error("$_");
390 54800         237458 };
391             }
392              
393             sub carp_ro {
394 423     423 1 1189 my ($name) = @_;
395             return sub {
396 676     676   71631 my ($orig, $self) = (shift, shift);
397 676 50       1778 carp "'$name' is a read-only accessor" if @_;
398 676         7500 return $self->$orig;
399 423         2746 };
400             }
401              
402             sub batch_alter_table_statements {
403 51     51 1 112 my ($diff_hash, $options, @meths) = @_;
404              
405 51 100       213 @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         105 my $package = caller;
419              
420             return map {
421 75 50       654 my $meth = $package->can($_) or die "$package cant $_";
422 75 100       135 map { $meth->(ref $_ eq 'ARRAY' ? @$_ : $_, $options) } @{ $diff_hash->{$_} }
  110         511  
  75         194  
423 51 100       95 } grep { @{$diff_hash->{$_} || []} }
  488         508  
  488         1002  
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