File Coverage

blib/lib/App/Sqitch/Engine/mysql.pm
Criterion Covered Total %
statement 136 167 81.4
branch 31 42 73.8
condition 10 25 40.0
subroutine 48 57 84.2
pod 14 14 100.0
total 239 305 78.3


line stmt bran cond sub pod time code
1             package App::Sqitch::Engine::mysql;
2              
3 4     4   305118 use 5.010;
  4         19  
4 4     4   33 use strict;
  4         11  
  4         124  
5 4     4   21 use warnings;
  4         11  
  4         194  
6 4     4   20 use utf8;
  4         8  
  4         35  
7 4     4   180 use Try::Tiny;
  4         11  
  4         330  
8 4     4   27 use App::Sqitch::X qw(hurl);
  4         8  
  4         53  
9 4     4   1765 use Locale::TextDomain qw(App-Sqitch);
  4         9  
  4         41  
10 4     4   1053 use App::Sqitch::Plan::Change;
  4         6  
  4         167  
11 4     4   24 use Path::Class;
  4         6  
  4         323  
12 4     4   29 use Moo;
  4         5  
  4         43  
13 4     4   2329 use App::Sqitch::Types qw(DBH URIDB ArrayRef Bool Str HashRef);
  4         6  
  4         63  
14 4     4   18999 use namespace::autoclean;
  4         6  
  4         47  
15 4     4   312 use List::MoreUtils qw(firstidx);
  4         12  
  4         121  
16              
17             extends 'App::Sqitch::Engine';
18              
19             our $VERSION = 'v1.6.1'; # VERSION
20              
21             has uri => (
22             is => 'ro',
23             isa => URIDB,
24             lazy => 1,
25             default => sub {
26             my $self = shift;
27             my $uri = $self->SUPER::uri;
28             $uri->host($ENV{MYSQL_HOST}) if !$uri->host && $ENV{MYSQL_HOST};
29             $uri->port($ENV{MYSQL_TCP_PORT}) if !$uri->_port && $ENV{MYSQL_TCP_PORT};
30             return $uri;
31             },
32             );
33              
34             has registry_uri => (
35             is => 'ro',
36             isa => URIDB,
37             lazy => 1,
38             default => sub {
39             my $self = shift;
40             my $uri = $self->uri->clone;
41             $uri->dbname($self->registry);
42             return $uri;
43             },
44             );
45              
46             sub registry_destination {
47 4     4 1 1034 my $uri = shift->registry_uri;
48 4 100       49 if ($uri->password) {
49 1         83 $uri = $uri->clone;
50 1         44 $uri->password(undef);
51             }
52 4         298 return $uri->as_string;
53             }
54              
55             has _mycnf => (
56             is => 'rw',
57             isa => HashRef,
58             default => sub {
59             eval 'require MySQL::Config; 1' or return {};
60             return scalar MySQL::Config::parse_defaults('my', [qw(client mysql)]);
61             },
62             );
63              
64 4 50   4   947 sub _def_user { $_[0]->_mycnf->{user} || $_[0]->sqitch->sysuser }
65 6 100   6   956 sub _def_pass { $ENV{MYSQL_PWD} || shift->_mycnf->{password} }
66              
67             sub _dsn {
68 3     3   4077 (my $dsn = shift->registry_uri->dbi_dsn) =~ s/\Adbi:mysql/dbi:MariaDB/;
69 3         2145 return $dsn;
70             }
71              
72             has dbh => (
73             is => 'rw',
74             isa => DBH,
75             lazy => 1,
76             default => sub {
77             my $self = shift;
78             $self->use_driver;
79             my $dbh = DBI->connect($self->_dsn, $self->username, $self->password, {
80             PrintError => 0,
81             RaiseError => 0,
82             AutoCommit => 1,
83             HandleError => $self->error_handler,
84             Callbacks => {
85             connected => sub {
86             my $dbh = shift;
87             $dbh->do("SET SESSION $_") or return for (
88             q{character_set_client = 'utf8'},
89             q{character_set_server = 'utf8'},
90             ($dbh->{mariadb_serverversion} || 0 < 50500 ? () : (q{default_storage_engine = 'InnoDB'})),
91             q{time_zone = '+00:00'},
92             q{group_concat_max_len = 32768},
93             q{sql_mode = '} . join(',', qw(
94             ansi
95             strict_trans_tables
96             no_auto_value_on_zero
97             no_zero_date
98             no_zero_in_date
99             only_full_group_by
100             error_for_division_by_zero
101             )) . q{'},
102             );
103             return;
104             },
105             },
106             });
107              
108             # Make sure we support this version.
109             my ($dbms, $vnum, $vstr) = $dbh->{mariadb_serverinfo} =~ /mariadb/i
110             ? ('MariaDB', 50300, '5.3')
111             : ('MySQL', 50100, '5.1.0');
112             hurl mysql => __x(
113             'Sqitch requires {rdbms} {want_version} or higher; this is {have_version}',
114             rdbms => $dbms,
115             want_version => $vstr,
116             have_version => $dbh->selectcol_arrayref('SELECT version()')->[0],
117             ) unless $dbh->{mariadb_serverversion} >= $vnum;
118              
119             return $dbh;
120             }
121             );
122              
123             has _ts_default => (
124             is => 'ro',
125             isa => Str,
126             lazy => 1,
127             default => sub {
128             return 'utc_timestamp(6)' if shift->_fractional_seconds;
129             return 'utc_timestamp';
130             },
131             );
132              
133             # Need to wait until dbh and _ts_default are defined.
134             with 'App::Sqitch::Role::DBIEngine';
135              
136             has _mysql => (
137             is => 'ro',
138             isa => ArrayRef,
139             lazy => 1,
140             default => sub {
141             my $self = shift;
142             my $uri = $self->uri;
143              
144             $self->sqitch->warn(__x
145             'Database name missing in URI "{uri}"',
146             uri => $uri
147             ) unless $uri->dbname;
148              
149             my @ret = ( $self->client );
150             # Use _port instead of port so it's empty if no port is in the URI.
151             # https://github.com/sqitchers/sqitch/issues/675
152             for my $spec (
153             [ user => $self->username ],
154             [ database => $uri->dbname ],
155             [ host => $uri->host ],
156             [ port => $uri->_port ],
157             ) {
158             push @ret, "--$spec->[0]" => $spec->[1] if $spec->[1];
159             }
160              
161             # Special-case --password, which requires = before the value. O_o
162             if (my $pw = $self->password) {
163             my $cfgpwd = $self->_mycnf->{password} || '';
164             push @ret, "--password=$pw" if $pw ne $cfgpwd;
165             }
166              
167             # Options to keep things quiet.
168             push @ret => (
169             (App::Sqitch::ISWIN ? () : '--skip-pager' ),
170             '--silent',
171             '--skip-column-names',
172             '--skip-line-numbers',
173             );
174              
175             # Get Maria to abort properly on error.
176             my $vinfo = try { $self->sqitch->probe($self->client, '--version') } || '';
177             if ($vinfo =~ /mariadb/i) {
178             my ($version) = $vinfo =~ /(?:Ver|client)\s+(\S+)/;
179             my ($maj, undef, $pat) = split /[.]/ => $version;
180             push @ret => '--abort-source-on-error'
181             if $maj > 5 || ($maj == 5 && $pat >= 66);
182             }
183              
184             # Add relevant query args.
185             if (my @p = $uri->query_params) {
186             my %option_for = (
187             mysql_compression => sub { $_[0] ? '--compress' : () },
188             mysql_ssl => sub { $_[0] ? '--ssl' : () },
189             mysql_connect_timeout => sub { '--connect_timeout', $_[0] },
190             mysql_init_command => sub { '--init-command', $_[0] },
191             mysql_socket => sub { '--socket', $_[0] },
192             mysql_ssl_client_key => sub { '--ssl-key', $_[0] },
193             mysql_ssl_client_cert => sub { '--ssl-cert', $_[0] },
194             mysql_ssl_ca_file => sub { '--ssl-ca', $_[0] },
195             mysql_ssl_ca_path => sub { '--ssl-capath', $_[0] },
196             mysql_ssl_cipher => sub { '--ssl-cipher', $_[0] },
197             );
198             while (@p) {
199             my ($k, $v) = (shift @p, shift @p);
200             my $code = $option_for{$k} or next;
201             push @ret => $code->($v);
202             }
203             }
204              
205             return \@ret;
206             },
207             );
208              
209             has _fractional_seconds => (
210             is => 'ro',
211             isa => Bool,
212             lazy => 1,
213             default => sub {
214             my $dbh = shift->dbh;
215             return $dbh->{mariadb_serverinfo} =~ /mariadb/i
216             ? $dbh->{mariadb_serverversion} >= 50305
217             : $dbh->{mariadb_serverversion} >= 50604;
218             },
219             );
220              
221 37     37 1 7137429 sub mysql { @{ shift->_mysql } }
  37         2327  
222              
223 7     7 1 14070 sub key { 'mysql' }
224 5     5 1 35 sub name { 'MySQL' }
225 1     1 1 4 sub driver { 'DBD::MariaDB 1.0' }
226 3     3 1 351 sub default_client { 'mysql' }
227              
228             sub _char2ts {
229 0     0   0 $_[1]->set_time_zone('UTC')->iso8601;
230             }
231              
232             sub _ts2char_format {
233 1     1   421 return q{date_format(%s, 'year:%%Y:month:%%m:day:%%d:hour:%%H:minute:%%i:second:%%S:time_zone:UTC')};
234             }
235              
236             sub _quote_idents {
237 0     0   0 shift;
238 0 0       0 map { $_ eq 'change' ? '"change"' : $_ } @_;
  0         0  
239             }
240              
241 0     0   0 sub _version_query { 'SELECT CAST(ROUND(MAX(version), 1) AS CHAR) FROM releases' }
242              
243             has initialized => (
244             is => 'ro',
245             isa => Bool,
246             lazy => 1,
247             writer => '_set_initialized',
248             default => sub {
249             my $self = shift;
250              
251             # Try to connect.
252             my $dbh = try { $self->dbh } catch {
253             # MySQL error code 1049 (ER_BAD_DB_ERROR): Unknown database '%-.192s'
254             return if $DBI::err && $DBI::err == 1049;
255             die $_;
256             } or return 0;
257              
258             return $dbh->selectcol_arrayref(q{
259             SELECT COUNT(*)
260             FROM information_schema.tables
261             WHERE table_schema = ?
262             AND table_name = ?
263             }, undef, $self->registry, 'changes')->[0];
264             }
265             );
266              
267             sub _initialize {
268 0     0   0 my $self = shift;
269 0 0       0 hurl engine => __x(
270             'Sqitch database {database} already initialized',
271             database => $self->registry,
272             ) if $self->initialized;
273              
274             # Create the Sqitch database if it does not exist.
275 0         0 (my $db = $self->registry) =~ s/"/""/g;
276 0         0 $self->_run(
277             '--execute' => sprintf(
278             'SET sql_mode = ansi; CREATE DATABASE IF NOT EXISTS "%s"',
279             $self->registry
280             ),
281             );
282              
283             # Deploy the registry to the Sqitch database.
284 0         0 $self->run_upgrade( file(__FILE__)->dir->file('mysql.sql') );
285 0         0 $self->_set_initialized(1);
286 0         0 $self->dbh->do('USE ' . $self->dbh->quote_identifier($self->registry));
287 0         0 $self->_create_check_function;
288 0         0 $self->_register_release;
289             }
290              
291             # Override to lock the Sqitch tables. This ensures that only one instance of
292             # Sqitch runs at one time.
293             sub begin_work {
294 0     0 1 0 my $self = shift;
295 0         0 my $dbh = $self->dbh;
296              
297             # Start transaction and lock all tables to disallow concurrent changes.
298             $dbh->do('LOCK TABLES ' . join ', ', map {
299 0         0 "$_ WRITE"
  0         0  
300             } qw(releases changes dependencies events projects tags));
301 0         0 $dbh->begin_work;
302 0         0 return $self;
303             }
304              
305             # We include the database name in the lock name because that's probably the most
306             # stringent lock the user expects. Locking the whole server with a static string
307             # prevents parallel deploys to other databases. Yes, locking just the target
308             # allows parallel deploys to conflict with one another if they make changes to
309             # other databases, but is not a great practice and likely an anti-pattern. So
310             # stick with the least surprising behavior.
311             # https://github.com/sqitchers/sqitch/issues/670
312             sub _lock_name {
313 3   50 3   1875 'sqitch working on ' . (shift->uri->dbname // '')
314             }
315              
316             # Override to try to acquire a lock on the string "sqitch working on $dbname"
317             # without waiting.
318             sub try_lock {
319 0     0 1 0 my $self = shift;
320             # Can't create a lock in the registry if it doesn't exist.
321 0 0       0 $self->initialize unless $self->initialized;
322 0         0 $self->dbh->selectcol_arrayref(
323             q{SELECT get_lock(?, ?)}, undef, $self->_lock_name, 0,
324             )->[0]
325             }
326              
327             # Override to try to acquire a lock on the string "sqitch working on $dbname",
328             # waiting for the lock until timeout.
329             sub wait_lock {
330 0     0 1 0 my $self = shift;
331 0         0 $self->dbh->selectcol_arrayref(
332             q{SELECT get_lock(?, ?)}, undef,
333             $self->_lock_name, $self->lock_timeout,
334             )->[0]
335             }
336              
337             # Override to unlock the tables, otherwise future transactions on this
338             # connection can fail.
339             sub finish_work {
340 0     0 1 0 my $self = shift;
341 0         0 my $dbh = $self->dbh;
342 0         0 $dbh->commit;
343 0         0 $dbh->do('UNLOCK TABLES');
344 0         0 return $self;
345             }
346              
347             sub _no_table_error {
348 4   66 4   2315 return $DBI::state && (
349             $DBI::state eq '42S02' # ER_BAD_TABLE_ERROR
350             ||
351             ($DBI::state eq '42000' && $DBI::err == '1049') # ER_BAD_DB_ERROR
352             )
353             }
354              
355             sub _no_column_error {
356 4   66 4   47 return $DBI::state && $DBI::state eq '42S22' && $DBI::err == '1054'; # ER_BAD_FIELD_ERROR
357             }
358              
359             sub _unique_error {
360 0   0 0   0 return $DBI::state && $DBI::state eq '23000' && $DBI::err == '1062'; # ER_DUP_ENTRY
361             }
362              
363 2     2   13 sub _regex_op { 'REGEXP' }
364              
365 2     2   24 sub _limit_default { '18446744073709551615' }
366              
367             sub _listagg_format {
368 1     1   8700 return q{GROUP_CONCAT(%1$s ORDER BY %1$s SEPARATOR ' ')};
369             }
370              
371             sub _prepare_to_log {
372 2     2   1489 my ($self, $table, $change) = @_;
373 2 100       11 return $self if $self->_fractional_seconds;
374              
375             # No sub-second precision, so delay logging a change until a second has passed.
376 1         12 my $dbh = $self->dbh;
377 1         11 my $sth = $dbh->prepare(qq{
378             SELECT UNIX_TIMESTAMP(committed_at) >= UNIX_TIMESTAMP()
379             FROM $table
380             WHERE project = ?
381             ORDER BY committed_at DESC
382             LIMIT 1
383             });
384 1         67 while ($dbh->selectcol_arrayref($sth, undef, $change->project)->[0]) {
385             # Sleep for 100 ms.
386 1         123 require Time::HiRes;
387 1         5 Time::HiRes::sleep(0.1);
388             }
389              
390 1         137 return $self;
391             }
392              
393             sub _set_vars {
394 17 100   17   96 my %vars = shift->variables or return;
395             return 'SET ' . join(', ', map {
396 5         71 (my $k = $_) =~ s/"/""/g;
  10         42  
397 10         30 (my $v = $vars{$_}) =~ s/'/''/g;
398 10         50 qq{\@"$k" = '$v'};
399             } sort keys %vars) . ";\n";
400             }
401              
402             sub _source {
403 12     12   4423 my ($self, $file) = @_;
404 12   100     39 my $set = $self->_set_vars || '';
405 12         219 return ('--execute' => "${set}source $file");
406             }
407              
408             sub _run {
409 6     6   2087 my $self = shift;
410 6         27 my $sqitch = $self->sqitch;
411 6 100       169 my $pass = $self->password or return $sqitch->run( $self->mysql, @_ );
412 1         179 local $ENV{MYSQL_PWD} = $pass;
413 1         13 return $sqitch->run( $self->mysql, @_ );
414             }
415              
416             sub _capture {
417 4     4   1227 my $self = shift;
418 4         21 my $sqitch = $self->sqitch;
419 4 100       101 my $pass = $self->password or return $sqitch->capture( $self->mysql, @_ );
420 1         22 local $ENV{MYSQL_PWD} = $pass;
421 1         5 return $sqitch->capture( $self->mysql, @_ );
422             }
423              
424             sub _spool {
425 5     5   2509 my $self = shift;
426 5         21 my @fh = (shift);
427 5         26 my $sqitch = $self->sqitch;
428 5 100       26 if (my $set = $self->_set_vars) {
429 2     1   117 open my $sfh, '<:utf8_strict', \$set;
  1         1132  
  1         21  
  1         8  
430 2         1406 unshift @fh, $sfh;
431             }
432 5 100       184 my $pass = $self->password or return $sqitch->spool( \@fh, $self->mysql, @_ );
433 2         90 local $ENV{MYSQL_PWD} = $pass;
434 2         16 return $sqitch->spool( \@fh, $self->mysql, @_ );
435             }
436              
437             sub run_file {
438 2     2 1 991 my $self = shift;
439 2         13 $self->_run( $self->_source(@_) );
440             }
441              
442             sub run_verify {
443 4     4 1 3982 my $self = shift;
444             # Suppress STDOUT unless we want extra verbosity.
445 4 100       117 my $meth = $self->can($self->sqitch->verbosity > 1 ? '_run' : '_capture');
446 4         186 $self->$meth( $self->_source(@_) );
447             }
448              
449             sub run_upgrade {
450 3     3 1 6084 my ($self, $file) = @_;
451 3         15 my @cmd = $self->mysql;
452              
453 3 100   24   52 if ((my $idx = firstidx { $_ eq '--database' } @cmd) > 0) {
  24         47  
454             # Replace the database name with the registry database.
455 1         59 $cmd[$idx + 1] = $self->registry;
456             } else {
457             # Append the registry database name.
458 2         85 push @cmd => '--database', $self->registry;
459             }
460              
461 3         268 return $self->sqitch->run(
462             @cmd,
463             $self->_source($self->_prepare_registry_file($file)),
464             );
465             }
466              
467             # Prepares $file for execution, editing its contents for various MySQL
468             # configurations.
469             sub _prepare_registry_file {
470 3     3   9 my ($self, $file) = @_;
471 3         13 my $has_frac = $self->_fractional_seconds;
472 3 100       23 return $file if $has_frac;
473              
474             # Read in the file to modify it.
475 2         23 my $sql = $file->slurp;
476              
477 2 50       983 if (!$has_frac) {
478             # Need to strip out datetime precision.
479 2         60 $sql =~ s{DATETIME\(\d+\)}{DATETIME}g;
480              
481             # Strip out 5.5 stuff on earlier versions.
482             $sql =~ s/-- ## BEGIN 5[.]5.+?-- ## END 5[.]5//ms
483 2 100       18 if $self->dbh->{mariadb_serverversion} < 50500;
484             }
485              
486             # Write out a temp file and return it.
487 2         48 require File::Temp;
488 2         18 my $fh = File::Temp->new;
489 2         2201 print $fh $sql;
490 2         164 close $fh;
491 2         35 return $fh;
492             }
493              
494             sub _create_check_function {
495             # The checkit() function works sort of like a CHECK: if the first argument
496             # is 0 or NULL, it throws the second argument as an exception.
497             # Conveniently, verify scripts can also use it to ensure an error is
498             # thrown when a change cannot be verified. Requires MySQL 5.5.0 and
499             # permission to create an immutable function, so ignore failures in those
500             # situations.
501 4     4   8448 my $self = shift;
502 4 100       16 return if $self->dbh->{mariadb_serverversion} < 50500;
503             try {
504 3     3   202 $self->dbh->do(q{
505             CREATE FUNCTION checkit(doit INTEGER, message VARCHAR(256)) RETURNS INTEGER DETERMINISTIC
506             BEGIN
507             IF doit IS NULL OR doit = 0 THEN
508             SIGNAL SQLSTATE 'ERR0R' SET MESSAGE_TEXT = message;
509             END IF;
510             RETURN doit;
511             END;
512             });
513             } catch {
514             # 1419: You do not have super user privilege and binary logging is
515             # enabled.
516 2 100 66 2   99 die $_ if !$DBI::err || $DBI::err != 1419;
517 1         14 $self->sqitch->warn(__(
518             'Insufficient permissions to create the checkit() function; skipping.',
519             ));
520             }
521 3         54 }
522              
523             sub run_handle {
524 2     2 1 1802 my ($self, $fh) = @_;
525 2         9 $self->_spool($fh);
526             }
527              
528             sub _cid {
529 1     1   627 my ( $self, $ord, $offset, $project ) = @_;
530              
531 1 50       14 my $offexpr = $offset ? " OFFSET $offset" : '';
532             return try {
533 1   0 1   58 return $self->dbh->selectcol_arrayref(qq{
534             SELECT change_id
535             FROM changes
536             WHERE project = ?
537             ORDER BY committed_at $ord
538             LIMIT 1$offexpr
539             }, undef, $project || $self->plan->project)->[0];
540             } catch {
541             # MySQL error code 1049 (ER_BAD_DB_ERROR): Unknown database '%-.192s'
542             # MySQL error code 1146 (ER_NO_SUCH_TABLE): Table '%s.%s' doesn't exist
543 1 0 0 1   34 return if $DBI::err && ($DBI::err == 1049 || $DBI::err == 1146);
      33        
544 1         19 die $_;
545 1         39 };
546             }
547              
548             1;
549              
550             __END__
551              
552             =head1 Name
553              
554             App::Sqitch::Engine::mysql - Sqitch MySQL Engine
555              
556             =head1 Synopsis
557              
558             my $mysql = App::Sqitch::Engine->load( engine => 'mysql' );
559              
560             =head1 Description
561              
562             App::Sqitch::Engine::mysql provides the MySQL storage engine for Sqitch. It
563             supports MySQL 5.1.0 and higher (best on 5.6.4 and higher), as well as MariaDB
564             5.3.0 and higher.
565              
566             =head1 Interface
567              
568             =head2 Instance Methods
569              
570             =head3 C<mysql>
571              
572             Returns a list containing the C<mysql> client and options to be passed to it.
573             Used internally when executing scripts. Query parameters in the URI that map
574             to C<mysql> client options will be passed to the client, as follows:
575              
576             =over
577              
578             =item * C<mysql_compression=1>: C<--compress>
579              
580             =item * C<mysql_ssl=1>: C<--ssl>
581              
582             =item * C<mysql_connect_timeout>: C<--connect_timeout>
583              
584             =item * C<mysql_init_command>: C<--init-command>
585              
586             =item * C<mysql_socket>: C<--socket>
587              
588             =item * C<mysql_ssl_client_key>: C<--ssl-key>
589              
590             =item * C<mysql_ssl_client_cert>: C<--ssl-cert>
591              
592             =item * C<mysql_ssl_ca_file>: C<--ssl-ca>
593              
594             =item * C<mysql_ssl_ca_path>: C<--ssl-capath>
595              
596             =item * C<mysql_ssl_cipher>: C<--ssl-cipher>
597              
598             =back
599              
600             =head3 C<username>
601              
602             =head3 C<password>
603              
604             Overrides the methods provided by the target so that, if the target has
605             no username or password, Sqitch looks them up in the
606             L<F</etc/my.cnf> and F<~/.my.cnf> files|https://dev.mysql.com/doc/refman/5.7/en/password-security-user.html>.
607             These files must limit access only to the current user (C<0600>). Sqitch will
608             look for a username and password under the C<[client]> and C<[mysql]>
609             sections, in that order.
610              
611             =head1 Author
612              
613             David E. Wheeler <david@justatheory.com>
614              
615             =head1 License
616              
617             Copyright (c) 2012-2026 David E. Wheeler, 2012-2021 iovation Inc.
618              
619             Permission is hereby granted, free of charge, to any person obtaining a copy
620             of this software and associated documentation files (the "Software"), to deal
621             in the Software without restriction, including without limitation the rights
622             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
623             copies of the Software, and to permit persons to whom the Software is
624             furnished to do so, subject to the following conditions:
625              
626             The above copyright notice and this permission notice shall be included in all
627             copies or substantial portions of the Software.
628              
629             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
630             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
631             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
632             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
633             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
634             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
635             SOFTWARE.
636              
637             =cut