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