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