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