File Coverage

blib/lib/SQL/Translator/Utils.pm
Criterion Covered Total %
statement 137 165 83.0
branch 57 88 64.7
condition 14 19 73.6
subroutine 33 34 97.0
pod 11 14 78.5
total 252 320 78.7


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