Initial commit
This commit is contained in:
commit
f4bfb0e367
71 changed files with 10399 additions and 0 deletions
652
Dam/Database.pm
Normal file
652
Dam/Database.pm
Normal file
|
@ -0,0 +1,652 @@
|
|||
=head1 NAME
|
||||
|
||||
Dam::Database
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
API for database access.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
package Dam::Database;
|
||||
|
||||
use Exporter qw(import);
|
||||
our @EXPORT = qw(
|
||||
UNKNOWN_COLUMN DUPLICATE_ENTRY SQL_SYNTAX_ERROR
|
||||
|
||||
database_connect database_disconnect database_last_err database_last_errstr
|
||||
CREATE_TEMP_TABLE DROP_TEMP_TABLE DROP_TEMP_TABLES
|
||||
QUERY UPDATE INSERT_INTO DELETE_FROM
|
||||
SELECT COUNT SUM AVG UNION
|
||||
FIELDS FROM JOINS WHERE GROUP_BY HAVING ORDER_BY LIMIT
|
||||
CLOSED AND OR NOT
|
||||
COMPARE COMPARE_STR COMPARE_DATE COMPARE_FIELDS
|
||||
BETWEEN BETWEEN_STR BETWEEN_DATES
|
||||
EXISTS IN_FIELD
|
||||
);
|
||||
|
||||
use DBI;
|
||||
|
||||
use Dam::Util;
|
||||
use Dam::Debug;
|
||||
|
||||
|
||||
|
||||
use constant {
|
||||
UNKNOWN_COLUMN => 1054,
|
||||
DUPLICATE_ENTRY => 1062,
|
||||
SQL_SYNTAX_ERROR => 1064
|
||||
};
|
||||
|
||||
my $DBH = undef; # Database handle
|
||||
my @TEMP_TABLES = (); # Temporal tables created during execution
|
||||
|
||||
|
||||
|
||||
=head2 database_connect()
|
||||
|
||||
Open connection to the database.
|
||||
|
||||
=cut
|
||||
|
||||
sub database_connect {
|
||||
my %arg = @_;
|
||||
check_arguments(\%arg,
|
||||
DB_DSN => [ ARG_REQUIRED ],
|
||||
DB_USER => [ ARG_REQUIRED ],
|
||||
DB_PASSWORD => [ ARG_REQUIRED ]
|
||||
);
|
||||
|
||||
$DBH = DBI->connect($arg{DB_DSN}, $arg{DB_USER}, $arg{DB_PASSWORD}) if !is_eq($arg{DB_DSN}, 'DBI:mysql:database=dbname;host=hostname');
|
||||
return $DBH;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 database_disconnect()
|
||||
|
||||
Close the connection to the database.
|
||||
|
||||
=cut
|
||||
|
||||
sub database_disconnect {
|
||||
$DBH->disconnect() if defined($DBH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 database_last_err()
|
||||
|
||||
Returns the exit code of the last database access statement executed.
|
||||
|
||||
=cut
|
||||
|
||||
sub database_last_err {
|
||||
return defined($DBH->err) ? $DBH->err : 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 database_last_errstr()
|
||||
|
||||
Returns the output text message of the last database access statement executed.
|
||||
|
||||
=cut
|
||||
|
||||
sub database_last_errstr {
|
||||
return defined($DBH->errstr) ? $DBH->errstr : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 CREATE_TEMP_TABLE($table, %SOURCE)
|
||||
|
||||
Create a temporary table in the database.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$table> (required): Name of the new data table.
|
||||
- B<%SOURCE> (required): Its structure. Can be:
|
||||
- C<DEF> => a definition of the fields in the new table;
|
||||
- C<LIKE> => the name of an existing table to copy its structure; or
|
||||
- C<AS> => a SELECT statement from which to get fields and data.
|
||||
|
||||
=cut
|
||||
|
||||
sub CREATE_TEMP_TABLE {
|
||||
my ($table, %SOURCE) = @_;
|
||||
|
||||
my $query = strval('
|
||||
CREATE TEMPORARY TABLE ', $table,
|
||||
!is_empty($SOURCE{DEF}) ? strval(' ( ', strval_join(', ', array($SOURCE{DEF})), ' )') : '',
|
||||
!is_empty($SOURCE{LIKE}) ? strval(' LIKE ', $SOURCE{LIKE}) : '',
|
||||
!is_empty($SOURCE{AS}) ? strval(' AS ( ', $SOURCE{AS}, ' )') : ''
|
||||
);
|
||||
$DBH->do($query);
|
||||
debug_info('CREATE TABLE', '<samp style="color: #888;">', $query, '</samp>');
|
||||
if ($DBH->err) {
|
||||
debug_error($DBH->errstr);
|
||||
}
|
||||
else {
|
||||
push(@TEMP_TABLES, $table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 DROP_TEMP_TABLE($table)
|
||||
|
||||
Drop a temporary table from the database.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$table> (required): Table name.
|
||||
|
||||
=cut
|
||||
|
||||
sub DROP_TEMP_TABLE {
|
||||
my $table = shift;
|
||||
|
||||
my $query = strval('
|
||||
DROP TEMPORARY TABLE ', $table
|
||||
);
|
||||
$DBH->do($query);
|
||||
debug_info('DROP TABLE', '<samp style="color: #888;">', $query, '</samp>');
|
||||
if ($DBH->err) {
|
||||
debug_error($DBH->errstr);
|
||||
}
|
||||
else {
|
||||
my $index = index_in_array($table, \@TEMP_TABLES);
|
||||
splice(@TEMP_TABLES, $index, 1) if $index != -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 DROP_TEMP_TABLES()
|
||||
|
||||
Delete the temporary tables created up to the moment of execution.
|
||||
|
||||
=cut
|
||||
|
||||
sub DROP_TEMP_TABLES {
|
||||
while (@TEMP_TABLES) {
|
||||
my $query = strval('
|
||||
DROP TEMPORARY TABLE ', pop(@TEMP_TABLES)
|
||||
);
|
||||
$DBH->do($query);
|
||||
debug_info('DROP TABLE', '<samp style="color: #888;">', $query, '</samp>');
|
||||
debug_error($DBH->errstr) if $DBH->err;
|
||||
}
|
||||
@TEMP_TABLES = ();
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 QUERY($query)
|
||||
|
||||
Execute a query on the database.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$query> (required): SELECT statement to execute.
|
||||
|
||||
=cut
|
||||
|
||||
sub QUERY {
|
||||
my $query = shift;
|
||||
|
||||
my $execute = $DBH->prepare($query);
|
||||
$execute->execute();
|
||||
my $rows = $execute->rows != -1 ? strval(' => <kbd>', $execute->rows, '</kbd> ', $execute->rows == 1 ? 'fila seleccionada.' : 'filas seleccionadas.') : '';
|
||||
debug_info('QUERY', '<samp style="color: #888;">', $query, '</samp>', $rows);
|
||||
debug_error($execute->errstr) if $execute->err;
|
||||
return $execute;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 UPDATE($table, %SET)
|
||||
|
||||
Run an update on the database.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$table> (required): Data table to be updated.
|
||||
- B<%SET>: Fields to update.
|
||||
|
||||
=cut
|
||||
|
||||
sub UPDATE {
|
||||
my ($table, %SET) = @_;
|
||||
|
||||
my $query = strval('
|
||||
UPDATE ', $table,
|
||||
!is_empty($SET{SET}) ? strval(' SET ', strval_join(', ', array($SET{SET}))) : '',
|
||||
WHERE(array($SET{WHERE}))
|
||||
);
|
||||
$DBH->do($query);
|
||||
debug_info('UPDATE', '<samp style="color: #888;">', $query, '</samp>');
|
||||
debug_error($DBH->errstr) if $DBH->err;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 INSERT_INTO($table, %INTO)
|
||||
|
||||
Insert data into a table in the database and return the number of rows inserted
|
||||
or -1 if any error occurred.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$table> (required): Table in which the data will be inserted.
|
||||
- B<%INTO>: Selection of data to insert.
|
||||
|
||||
=cut
|
||||
|
||||
sub INSERT_INTO {
|
||||
my ($table, %INTO) = @_;
|
||||
|
||||
my $query = strval('
|
||||
INSERT INTO ', $table,
|
||||
!is_empty($INTO{FIELDS}) ? strval(' ( ', strval_join(', ', array($INTO{FIELDS})), ' )') : '',
|
||||
!is_empty($INTO{VALUES}) ? strval(' VALUES ( ', strval_join(', ', array($INTO{VALUES})), ' )') : '',
|
||||
is_eq(ref($INTO{SELECT}), 'HASH') ? SELECT(%{$INTO{SELECT}}) : strval(' ', $INTO{SELECT})
|
||||
);
|
||||
my $rows = $DBH->do($query);
|
||||
$rows = -1 if $DBH->err;
|
||||
$rows = 0 if is_eq($rows, '0E0');
|
||||
my $inserted_rows = $rows != -1 ? strval(' => <kbd>', $rows, '</kbd> ', $rows == 1 ? 'fila insertada.' : 'filas insertadas.') : '';
|
||||
debug_info('INSERT INTO', '<samp style="color: #888;">', $query, '</samp>', $inserted_rows);
|
||||
debug_error($DBH->errstr) if $DBH->err;
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 DELETE_FROM($table, %WHERE)
|
||||
|
||||
Delete a set of records from the database.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<$table> (required): Table from which records will be deleted.
|
||||
- B<%WHERE>: Conditions to delete.
|
||||
|
||||
=cut
|
||||
|
||||
sub DELETE_FROM {
|
||||
my ($table, %WHERE) = @_;
|
||||
|
||||
my $query = strval('
|
||||
DELETE FROM ', $table,
|
||||
WHERE(array($WHERE{WHERE}))
|
||||
);
|
||||
$DBH->do($query);
|
||||
debug_info('DELETE FROM', '<samp style="color: #888;">', $query, '</samp>');
|
||||
debug_error($DBH->errstr) if $DBH->err;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 SELECT(%SELECT)
|
||||
|
||||
Construct a valid SELECT statement with the input arguments.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<%SELECT>: SELECT statement elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub SELECT {
|
||||
my %SELECT = @_;
|
||||
|
||||
return strval('
|
||||
SELECT ',
|
||||
FIELDS(array($SELECT{FIELDS})),
|
||||
FROM(array($SELECT{FROM})),
|
||||
JOINS(array($SELECT{JOINS})),
|
||||
WHERE(array($SELECT{WHERE})),
|
||||
GROUP_BY(array($SELECT{GROUP_BY})),
|
||||
HAVING($SELECT{HAVING}),
|
||||
ORDER_BY(array($SELECT{ORDER_BY})),
|
||||
LIMIT($SELECT{LIMIT})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 COUNT(%SELECT)
|
||||
|
||||
Constructs and executes a valid SELECT COUNT(C<$SELECT{FIELDS}>) statement with
|
||||
the input arguments and returns the number of records that apply.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<%SELECT>: SELECT statement elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub COUNT {
|
||||
my %SELECT = @_;
|
||||
|
||||
$SELECT{FIELDS} = strval('COUNT(', is_empty($SELECT{FIELDS}) ? '*' : $SELECT{FIELDS}, ')');
|
||||
|
||||
my $query = SELECT(%SELECT);
|
||||
|
||||
my $execute = $DBH->prepare($query);
|
||||
$execute->execute();
|
||||
my $count = numval(($execute->fetchrow_array())[0]);
|
||||
debug_info('COUNT', '<samp style="color: #888;">', $query, '</samp> => Cuenta <kbd>', $count, '</kbd>');
|
||||
debug_error($execute->errstr) if $execute->err;
|
||||
return $count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 SUM(%SELECT)
|
||||
|
||||
Constructs and executes a valid SELECT SUM(C<$SELECT{FIELDS}>) statement with
|
||||
the input arguments and returns the requested sum.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<%SELECT>: SELECT statement elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub SUM {
|
||||
my %SELECT = @_;
|
||||
|
||||
$SELECT{FIELDS} = !is_empty($SELECT{FIELDS}) ? strval('SUM(', $SELECT{FIELDS}, ')') : '';
|
||||
|
||||
my $query = SELECT(%SELECT);
|
||||
|
||||
my $execute = $DBH->prepare($query);
|
||||
$execute->execute();
|
||||
my $sum = numval(($execute->fetchrow_array())[0]);
|
||||
debug_info('SUM', '<samp style="color: #888;">', $query, '</samp> => Suma <kbd>', $sum, '</kbd>');
|
||||
debug_error($execute->errstr) if $execute->err;
|
||||
return $sum;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 AVG(%SELECT)
|
||||
|
||||
Constructs and executes a valid SELECT AVG(C<$SELECT{FIELDS}>) statement with
|
||||
the input arguments and returns the requested average.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<%SELECT>: SELECT statement elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub AVG {
|
||||
my %SELECT = @_;
|
||||
|
||||
$SELECT{FIELDS} = !is_empty($SELECT{FIELDS}) ? strval('AVG(', $SELECT{FIELDS}, ')') : '';
|
||||
|
||||
my $query = SELECT(%SELECT);
|
||||
|
||||
my $execute = $DBH->prepare($query);
|
||||
$execute->execute();
|
||||
my $avg = numval(($execute->fetchrow_array())[0]);
|
||||
debug_info('AVG', '<samp style="color: #888;">', $query, '</samp> => Media <kbd>', $avg, '</kbd>');
|
||||
debug_error($execute->errstr) if $execute->err;
|
||||
return $avg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 UNION(@SELECTS)
|
||||
|
||||
Concatenates a list of SELECT statements into a valid UNION sentence.
|
||||
|
||||
=head3 Arguments:
|
||||
|
||||
- B<@SELECTS>: SELECT statement list.
|
||||
|
||||
=cut
|
||||
|
||||
sub UNION {
|
||||
return trim(strval_join(' UNION ', @_));
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub FIELDS {
|
||||
my $fields = trim(strval_join(', ', @_));
|
||||
return !is_empty($fields) ? strval(' ', $fields) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub FROM {
|
||||
my $from = trim(strval_join(', ', @_));
|
||||
return !is_empty($from) ? strval(' FROM ', $from) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub JOINS {
|
||||
my $joins = trim(strval_join(' ', @_));
|
||||
return !is_empty($joins) ? strval(' ', $joins) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub WHERE {
|
||||
my $where = trim(AND(@_));
|
||||
return !is_empty($where) ? strval(' WHERE ', $where) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub GROUP_BY {
|
||||
my $group_by = trim(strval_join(', ', @_));
|
||||
return !is_empty($group_by) ? strval(' GROUP BY ', $group_by) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub HAVING {
|
||||
my $having = trim(shift);
|
||||
return !is_empty($having) ? strval(' HAVING ', $having) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub ORDER_BY {
|
||||
my $order_by = trim(strval_join(', ', @_));
|
||||
return !is_empty($order_by) ? strval(' ORDER BY ', $order_by) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub LIMIT {
|
||||
my $limit = trim(shift);
|
||||
return !is_empty($limit) ? strval(' LIMIT ', $limit) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub CLOSED {
|
||||
my $sentence = trim(shift);
|
||||
return !is_empty($sentence) ? strval(' ( ', $sentence, ' )') : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub AND {
|
||||
my $conditions = trim(strval_join(' AND ', @_));
|
||||
return !is_empty($conditions) ? strval(' ', $conditions) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub OR {
|
||||
my $conditions = trim(strval_join(' OR ', @_));
|
||||
return !is_empty($conditions) ? strval(' ', $conditions) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub NOT {
|
||||
my $sentence = trim(shift);
|
||||
return !is_empty($sentence) ? strval(' NOT ', $sentence) : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub COMPARE {
|
||||
my ($field, $op, $value) = @_;
|
||||
|
||||
$field = trim($field);
|
||||
$op = trim(one_space(uc($op)));
|
||||
return '' if is_empty($field) || is_empty($op);
|
||||
$value = trim($value);
|
||||
if (is_empty($value)) {
|
||||
return is_eq($op, 'IS NULL') || is_eq($op, 'IS NOT NULL') ? strval(' ', $field, ' ', $op) : '';
|
||||
}
|
||||
return strval(' ', $field, ' IS NULL') if is_eq($op, '=') && is_eq(uc($value), 'NULL');
|
||||
return strval(' ', $field, ' IS NOT NULL') if is_eq($op, '!=') && is_eq(uc($value), 'NULL');
|
||||
return strval(' ', $field, ' ', $op, ' ', $value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub COMPARE_STR {
|
||||
my ($field, $op, $string) = @_;
|
||||
|
||||
$field = trim($field);
|
||||
$op = trim(one_space(uc($op)));
|
||||
return '' if is_empty($field) || is_empty($op);
|
||||
if (is_empty(trim($string))) {
|
||||
return is_eq($op, 'IS NULL') || is_eq($op, 'IS NOT NULL') ? strval(' ', $field, ' ', $op) : '';
|
||||
}
|
||||
return strval(' ', $field, ' IS NULL') if is_eq($op, '=') && is_eq(uc($string), 'NULL');
|
||||
return strval(' ', $field, ' IS NOT NULL') if is_eq($op, '!=') && is_eq(uc($string), 'NULL');
|
||||
return strval(' ', $field, ' ', $op, " '", $string, "'");
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub COMPARE_DATE {
|
||||
return COMPARE_STR(@_);
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub COMPARE_FIELDS {
|
||||
return COMPARE(@_);
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub BETWEEN {
|
||||
my ($field, $ini, $end) = @_;
|
||||
|
||||
my $c1 = COMPARE($field, '>=', $ini);
|
||||
my $c2 = COMPARE($field, '<=', $end);
|
||||
return is_empty($c1) || is_empty($c2) ? strval($c1, $c2) : strval(' (', $c1, ' AND', $c2, ' )');
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub BETWEEN_STR {
|
||||
my ($field, $ini, $end) = @_;
|
||||
|
||||
my $c1 = COMPARE_STR($field, '>=', $ini);
|
||||
my $c2 = COMPARE_STR($field, '<=', $end);
|
||||
return is_empty($c1) || is_empty($c2) ? strval($c1, $c2) : strval(' (', $c1, ' AND', $c2, ' )');
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub BETWEEN_DATES {
|
||||
return BETWEEN_STR(@_);
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub EXISTS {
|
||||
my $sentence = shift;
|
||||
return !is_empty($sentence) ? strval(' EXISTS ( ', $sentence, ' )') : '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub IN_FIELD {
|
||||
my ($field, $values) = @_;
|
||||
|
||||
$field = trim($field);
|
||||
$values = trim($values);
|
||||
|
||||
return '' if is_empty($field) || is_empty($values);
|
||||
|
||||
my $isnot = is_eq(substr($values, 0, 1), '!');
|
||||
$values = substr($values, 1) if $isnot;
|
||||
|
||||
my @values = split(m/('[^']+'|"[^"]+"|[^,]+)(?:\s*,\s*)?/, $values);
|
||||
|
||||
my $isnull = '';
|
||||
my @nulls = ("'NULL'", '"NULL"', 'NULL');
|
||||
my @infield = ();
|
||||
foreach my $value (@values) {
|
||||
if (!is_empty($value)) {
|
||||
if (in_array($value, \@nulls)) {
|
||||
$isnull = $isnot ? "$field IS NOT NULL" : "$field IS NULL";
|
||||
}
|
||||
else {
|
||||
push(@infield, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isnull if scalar(@infield) == 0;
|
||||
my $infield = strval($field, $isnot ? ' NOT IN ( ' : ' IN ( ', strval_join(', ', @infield), ' )');
|
||||
return is_empty($isnull) ? $infield : strval("( $infield ", $isnot ? 'AND' : 'OR', " $isnull )");
|
||||
}
|
||||
|
||||
|
||||
|
||||
1;
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Manuel Cillero C<< <manuel@cillero.es> >>
|
||||
|
||||
=head1 COPYRIGHT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2004-2020 Manuel Cillero. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
=cut
|
Reference in a new issue