Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

use strict;
use utf8;
=encoding utf8
=head1 NAME
Acrux::DBI::Dump - Working with SQL dumps
=head1 SYNOPSIS
use Acrux::DBI::Dump;
my $dump = Acrux::DBI::Dump->new(
dbi => $dbi
);
$dump->from_file('/tmp/test.sql')->poke;
=head1 DESCRIPTION
This class is used by L<Acrux::DBI> to allow database schemas import.
A dump file is just a collection of sql blocks, with one or more statements, separated by comments of the form
C<-- #NAME> or C<-- # NAME>
-- #foo
CREATE TABLE `pets` (`pet` TEXT);
INSERT INTO `pets` VALUES ('cat');
INSERT INTO `pets` VALUES ('dog');
delimiter //
CREATE PROCEDURE `test`()
BEGIN
SELECT `pet` FROM `pets`;
END
//
-- #bar
DROP TABLE `pets`;
DROP PROCEDURE `test`;
-- #baz (...you can comment freely here...)
-- ...and here...
CREATE TABLE `stuff` (`whatever` INT);
-- #main
DROP TABLE `stuff`;
This idea is to let you import SQL dumps step by step by its names
=head1 ATTRIBUTES
This class implements the following attributes
=head2 dbi
$dump = $dump->dbi($dbi);
my $dbi = $dump->dbi;
The object these processing belong to
=head2 name
my $name = $dump->name;
$dump = $dump->name('foo');
Name for this dump, defaults to C<schema>
=head1 METHODS
This class implements all methods from L<Mojo::Base> and implements
the following new ones
=head2 from_data
$dump = $dump->from_data;
$dump = $dump->from_data('main');
$dump = $dump->from_data('main', 'file_name');
Extract dump data from a file in the DATA section of a class with
L<Mojo::Loader/"data_section">, defaults to using the caller class and
L</"name">.
__DATA__
@@ schema
-- # up
CREATE TABLE `pets` (`pet` TEXT);
INSERT INTO `pets` VALUES ('cat');
INSERT INTO `pets` VALUES ('dog');
-- # down
DROP TABLE `pets`
=head2 from_file
$dump = $dump->from_file('/tmp/schema.sql');
Read dump data from a file
=head2 from_string
$dump = $dump->from_string('
-- # up
CREATE TABLE `pets` (`pet` TEXT);
-- # down
DROP TABLE `pets`
');
Read dump data from string
=head2 peek
my $sqls = $dump->peek; # 'main'
my $sqls = $dump->peek('foo');
my @sqls = $dump->peek('foo');
This method returns an array/arrayref of SQL statements stored at a specified dump location by tag-name.
By default will be used the C<main> tag
=head2 poke
$dump = $dump->poke; # 'main'
$dump = $dump->poke('foo');
Import named data-block of SQL dump to database by tag-name. By default will be used the C<main> tag
=head1 HISTORY
See C<Changes> file
=head1 TO DO
See C<TODO> file
=head1 SEE ALSO
L<Acrux::DBI>, L<Mojo::mysql>, L<Mojo::Pg>
=head1 AUTHOR
Serż Minus (Sergey Lepenkov) L<https://www.serzik.com> E<lt>abalama@cpan.orgE<gt>
=head1 COPYRIGHT
Copyright (C) 1998-2024 D&D Corporation. All Rights Reserved
=head1 LICENSE
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See C<LICENSE> file and L<https://dev.perl.org/licenses/>
=cut
our $VERSION = '0.01';
use Mojo::Base -base;
use Mojo::Loader qw/data_section/;
use Mojo::File qw/path/;
use constant {
DELIMITER => ';',
TAG_DEFAULT => 'main',
};
has name => 'schema';
has 'dbi';
has 'pool' => sub {{}};
sub from_string {
my $self = shift;
my $s = shift;
return $self unless defined $s;
my $pool = $self->{pool} = {};
my $tag = TAG_DEFAULT;
my $delimiter = DELIMITER;
my $is_new = 1;
my $buf = '';
# String processing
while (length($s)) {
my $chunk;
# get fragments (chunks) from string
if ($s =~ /^$delimiter/x) { # any delimiter char(s)
$is_new = 1;
$chunk = $delimiter;
} elsif ($s =~ /^delimiter\s+(\S+)\s*(?:\n|\z)/ip) { # set new delimiter
$is_new = 1;
$chunk = ${^MATCH};
$delimiter = $1;
} elsif ($s =~ /^(\s+)/s or $s =~ /^(\w+)/) { # whitespaces or general name
$chunk = $1;
} elsif ($s =~ /^--.*(?:\n|\z)/p # double-dash comment
or $s =~ /^\#.*(?:\n|\z)/p # hash comment
or $s =~ /^\/\*(?:[^\*]|\*[^\/])*(?:\*\/|\*\z|\z)/p # C-style comment
or $s =~ /^'(?:[^'\\]*|\\(?:.|\n)|'')*(?:'|\z)/p # single-quoted literal text
or $s =~ /^"(?:[^"\\]*|\\(?:.|\n)|"")*(?:"|\z)/p # double-quoted literal text
or $s =~ /^`(?:[^`]*|``)*(?:`|\z)/p ) { # schema-quoted literal text
$chunk = ${^MATCH};
} else {
$chunk = substr($s, 0, 1);
}
#say STDERR ">$chunk<";
# cut string by chunk length
substr($s, 0, length($chunk), '');
# marker
if ($chunk =~ /^--\s+[#]+\s*(\w+)/i) {
my $_tag = $1 // TAG_DEFAULT;
push @{$pool->{$tag} //= []}, $buf if length($tag) and $buf !~ /^\s*$/s;
$tag = $_tag;
$is_new = 0;
$buf = '';
$delimiter = DELIMITER; # flush delimiter to default
}
# make new block
if ($is_new) {
push @{$pool->{$tag} //= []}, $buf if length($tag) and $buf !~ /^\s*$/s;
$is_new = 0;
$buf = '';
} else { # Or add cur chunk to section
$buf .= $chunk;
}
}
# add buf line to block
push @{$pool->{$tag} //= []}, $buf if length($tag) and $buf !~ /^\s*$/s;
return $self;
}
sub from_data {
my $self = shift;
my $class = shift;
my $name = shift;
return $self->from_string(data_section($class //= caller, $name // $self->name));
}
sub from_file {
my $self = shift;
my $file = shift;
return $self->from_string(path($file)->slurp('UTF-8'));
}
sub poke {
my $self = shift;
my $tag = shift || TAG_DEFAULT;
my $sqls = $self->pool->{$tag} || [];
my $dbi = $self->dbi;
return $self unless $dbi and $dbi->ping;
# Import statements
foreach my $sql (@$sqls) {
#print STDERR $sql, "\n";
$dbi->query($sql) or last;
}
return $self;
}
sub peek {
my $self = shift;
my $tag = shift || TAG_DEFAULT;
my $sqls = $self->pool->{$tag} || [];
return wantarray ? (@$sqls) : [@$sqls]; # copy of data
}
1;
__END__