652 lines
14 KiB
Perl
652 lines
14 KiB
Perl
=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 = uc(trim(one_space($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 = uc(trim(one_space($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
|