File Coverage

blib/lib/SQLite/Archive.pm
Criterion Covered Total %
statement 72 84 85.7
branch 9 22 40.9
condition n/a
subroutine 15 17 88.2
pod 0 6 0.0
total 96 129 74.4


line stmt bran cond sub pod time code
1             package SQLite::Archive;
2              
3             =pod
4              
5             =head1 NAME
6              
7             SQLite::Archive - Version-agnostic storage and manipulation of SQLite databases
8              
9             =head1 DESCRIPTION
10              
11             SQLite (and the Perl module for it L) is an extremely handy
12             database for storing various types of simple information.
13              
14             However, as SQLite has developed, the binary structure of the SQLite
15             database format itself has changed and evolved, and continues to change
16             and evolve. As new releases come out, new versions of L are
17             also released with matching code.
18              
19             This makes SQLite database files suboptimal (at best) for use in
20             distributing data sets between disparate systems.
21              
22             At the same time, a giant raw .sql script says very little about the
23             data itself (such as which database and version it is intended for),
24             requires a client front end to throw the SQL script at, and it not
25             easily editable or manipulatable while dumped.
26              
27             B provides a straight forward mechanism for exporting
28             (and importing) SQLite databases, and moving that data around as a
29             single file to (or from) other hosts.
30              
31             It uses a regular tar archive, with the data stored in CSV files, and
32             the table structure stored in a create.sql file.
33              
34             Given a SQLite archive file (B will take anything
35             supported by L) it will extract the tarball to a
36             temporary directory, create a SQLite database (in a location of your
37             choice or also in a temp directory) and then populate the SQLite
38             database with the data from the archive.
39              
40             =head1 METHODS
41              
42             =cut
43              
44 2     2   25922 use 5.005;
  2         7  
  2         113  
45 2     2   11 use strict;
  2         3  
  2         64  
46 2     2   18 use Carp 'croak';
  2         3  
  2         111  
47 2     2   9 use File::Spec ();
  2         3  
  2         39  
48 2     2   4168 use File::Temp ();
  2         69309  
  2         47  
49 2     2   1975 use Archive::Extract ();
  2         389278  
  2         101  
50 2     2   1922 use SQL::Script ();
  2         10054  
  2         49  
51 2     2   1919 use Parse::CSV ();
  2         199660  
  2         51  
52 2     2   273267 use DBI ();
  2         38706  
  2         92  
53              
54 2     2   38 use vars qw{$VERSION};
  2         4  
  2         124  
55             BEGIN {
56 2     2   2048 $VERSION = '0.02';
57             }
58              
59              
60              
61              
62              
63             #####################################################################
64             # Constructor and Accessors
65              
66             =pod
67              
68             =head1 new
69              
70             SQLite::Archive->new( file => 'data.tar.gz' );
71             SQLite::Archive->new( file => 'data.zip' );
72             SQLite::Archive->new( dir => 'extracted' );
73              
74             The C constructor creates a new SQLite archive object.
75              
76             It takes a data source as either a C param (which should be
77             an L-compatible archive, or a C param (which
78             should contain the equivalent of the content of the archive, but
79             already expanded as single files).
80              
81             Returns a new B object, or throws an exception
82             on error.
83              
84             =cut
85              
86             sub new {
87 1     1 0 918 my $class = shift;
88 1         6 my $self = bless { @_ }, $class;
89              
90             # Check the archive directory
91 1 50       8 unless ( defined $self->dir ) {
92             # Check the archive file
93 0 0       0 unless ( -f $self->file ) {
94 0         0 croak("The file '" . $self->file . "' does not exist");
95             }
96              
97             # Extract the archive
98 0         0 my $archive = Archive::Extract->new( archive => $self->file );
99 0         0 my $tempdir = File::Temp::tempdir( CLEANUP => 1 );
100 0 0       0 $archive->extract( to => $tempdir ) or die $archive->error;
101 0         0 $self->{dir} = $archive->extract_path;
102             }
103 1 50       5 unless ( -d $self->dir ) {
104 0         0 croak("The directory '" . $self->dir . "' does not exist");
105             }
106              
107             # Locate all data files
108 1 50       4 opendir( ARCHIVE, $self->dir ) or die "opendir: $!";
109 1         31 my @files = sort readdir( ARCHIVE );
110 1 50       18 closedir( ARCHIVE ) or die "closedir: $!";
111 1         4 $self->{sql} = [ grep { /^\w+\.sql/ } @files ];
  4         20  
112 1         3 $self->{csv} = [ grep { /^\w+\.csv/ } @files ];
  4         15  
113              
114 1         4 return $self;
115             }
116              
117             sub uri {
118 0     0 0 0 $_[0]->{uri};
119             }
120              
121             sub file {
122 0     0 0 0 $_[0]->{file};
123             }
124              
125             sub dir {
126 4     4 0 91 $_[0]->{dir};
127             }
128              
129              
130              
131              
132              
133             #####################################################################
134             # Main Methods
135              
136             =pod
137              
138             =head1 create_db
139              
140             $dbh = $archive->create_db; # Temp file created
141             $dbh = $archive->create_db( 'dir/sqlite.db' );
142              
143             The C method create a new (empty) SQLite database.
144              
145             It optionally takes a single param of a path at which it should
146             create the SQLite file.
147              
148             If created as a temp file, the database file will be destroyed
149             until END-time (as opposed to being destroyed when the DBI
150             connection handle goes out of scope).
151              
152             Returns a L connection (as a B object) or throws
153             an exception on error.
154              
155             =cut
156              
157             sub create_db {
158 1     1 0 2 my $self = shift;
159 1         2 my $file = undef;
160 1 50       6 if ( @_ ) {
161             # Explicit file name
162 0         0 die "CODE INCOMPLETE";
163             } else {
164             # Get a temp file name
165 1         7 my $dir = File::Temp::tempdir( CLEANUP => 1 );
166 1         767 $file = File::Spec->catfile( $dir, 'sqlite.db' );
167             }
168              
169             # Create the database
170 1         16 my $db = DBI->connect( 'dbi:SQLite:' . $file );
171 1 50       22654 unless ( $db ) {
172 0         0 croak("Failed to create test DB handle");
173             }
174              
175 1         4 return $db;
176             }
177              
178             =pod
179              
180             =head1 build_db
181              
182             $dbh = $archive->build_db; # Temp file created
183             $dbh = $archive->build_db( 'dir/sqlite.db' );
184              
185             The C method provides the main functionality for SQLite::Archive.
186              
187             It creates a new SQLite database (at a temporary file if needed), executes
188             any SQL scripts, populates tables from any CSV files, and returns a DBI
189             handle.
190              
191             Returns a BDI::db object, or throws an exception on error.
192              
193             =cut
194              
195             sub build_db {
196 1     1 0 608 my $self = shift;
197 1         5 my $dbh = $self->create_db(@_);
198              
199             # Execute any SQL files first, in order
200 1         7 my $dir = $self->dir;
201 1         3 foreach my $sql ( @{$self->{sql}} ) {
  1         6  
202 1         46 my $file = File::Spec->catfile( $dir, $sql );
203              
204             # Load the script
205 1         12 my $script = SQL::Script->new;
206 1         29 $script->read( $file );
207              
208             # Execute the script
209 1         564 $script->run( $dbh );
210             }
211              
212             # Now parse and insert any CSV data
213 1         140691 foreach my $csv ( @{$self->{csv}} ) {
  1         6  
214 1         24 my $file = File::Spec->catfile( $dir, $csv );
215              
216             # Create the parser for the file
217 1 50       13 my $parser = Parse::CSV->new(
218             file => $file,
219             fields => 'auto',
220             ) or die "Failed to create CSV::Parser for $csv";
221 1         545 my (undef, undef, $table) = File::Spec->splitpath($file);
222 1 50       9 $table =~ s/\.csv$// or die "Failed to find table name";
223              
224             # Process the inserts
225             # Don't bother chunking for now, just auto-commit.
226 1         9 while ( my $row = $parser->fetch ) {
227 4         14 my $sql = "INSERT INTO $table ( "
228             . join( ', ', keys %$row )
229             . " ) values ( "
230 2         22750 . join( ', ', map { '?' } values %$row )
231             . " )";
232 2 50       31 $dbh->do( $sql, {}, values %$row ) and next;
233 0         0 die "Table insert failed in $csv: $DBI::errstr";
234             }
235             }
236              
237 1         13295 return $dbh;
238             }
239              
240             1;
241              
242             __END__