File Coverage

blib/lib/DBIx/Export.pm
Criterion Covered Total %
statement 109 120 90.8
branch 14 26 53.8
condition 4 5 80.0
subroutine 15 15 100.0
pod 0 4 0.0
total 142 170 83.5


line stmt bran cond sub pod time code
1             package DBIx::Export;
2              
3             =pod
4              
5             =head1 NAME
6              
7             DBIx::Export - Export data from DBI as a SQLite database
8              
9             =head1 SYNOPSIS
10              
11             my $export = DBIx::Export->new(
12             file => 'publish.sqlite',
13             source => DBI->connect($dsn, $user, $pass),
14             );
15            
16             $export->table( 'table1',
17             'select * from foo where this < 10',
18             );
19            
20             $export->finish;
21              
22             =head1 DESCRIPTION
23              
24             B
25              
26             This is an experimental module that automates the exporting of data from
27             arbitrary DBI handles to a SQLite file suitable for publishing online
28             for others to download.
29              
30             It takes a set of queries, analyses the data returned by the query,
31             then creates a table in the output SQLite database.
32              
33             In the process, it also ensures all the optimal pragmas are set,
34             an index is places on every column in every table, and the database
35             is fully vacuumed.
36              
37             As a result, you should be able to connect to any arbitrary datasource
38             using any arbitrary DBI driver and then map an arbitrary series of
39             SQL queries like views into the published SQLite database.
40              
41             =cut
42              
43 2     2   38778 use 5.006;
  2         9  
  2         83  
44 2     2   13 use strict;
  2         5  
  2         74  
45 2     2   21 use warnings;
  2         3  
  2         70  
46 2     2   1149 use bytes ();
  2         20  
  2         61  
47 2     2   11 use Carp 'croak';
  2         3  
  2         152  
48 2     2   1951 use Params::Util 0.33 ();
  2         9989  
  2         107  
49 2     2   9004 use DBI 1.57 ();
  2         49008  
  2         112  
50 2     2   3129 use DBD::SQLite 1.21 ();
  2         26965  
  2         69  
51              
52 2     2   21 use vars qw{$VERSION};
  2         4  
  2         106  
53             BEGIN {
54 2     2   65 $VERSION = '0.01';
55             }
56              
57 2         12 use Object::Tiny 1.06 qw{
58             file
59             source
60             dbh
61 2     2   1986 };
  2         700  
62              
63              
64              
65              
66              
67             #####################################################################
68             # Constructor
69              
70             sub new {
71 1     1 0 6229121 my $class = shift;
72 1         8 my $self = bless { @_ }, $class;
73              
74             # Connect to the SQLite database
75 1         49 my $dsn = "DBI:SQLite:" . $self->file;
76 1         29 $self->{dbh} = DBI->connect( $dsn, '', '', {
77             PrintError => 1,
78             RaiseError => 1,
79             } );
80              
81             # Maximise compatibility
82 1         776 $self->sqlite('PRAGMA legacy_file_format = 1');
83              
84             # Turn on all the go-faster pragmas
85 1         113 $self->sqlite('PRAGMA synchronous = 0');
86 1         462 $self->sqlite('PRAGMA temp_store = 2');
87 1         78 $self->sqlite('PRAGMA journal_mode = OFF');
88 1         77 $self->sqlite('PRAGMA locking_mode = EXCLUSIVE');
89              
90             # Disable auto-vacuuming because we'll only fill this once.
91             # Do a one-time vacuum so we start with a clean empty database.
92 1         78 $self->sqlite('PRAGMA auto_vacuum = 0');
93 1         73 $self->sqlite('VACUUM');
94              
95 1         769 return $self;
96             }
97              
98             # Execute a query on the sqlite database
99             sub sqlite {
100 23     23 0 133 shift->{dbh}->do(@_);
101             }
102              
103             # Clean up the SQLite database
104             sub finish {
105 1     1 0 3 my $self = shift;
106              
107             # Tidy up the database
108 1         3 $self->sqlite('PRAGMA synchronous = NORMAL');
109 1         124 $self->sqlite('PRAGMA temp_store = 0');
110 1         70 $self->sqlite('PRAGMA locking_mode = NORMAL');
111 1         136 $self->sqlite('VACUUM');
112              
113             # Disconnect
114 1         24825 $self->{dbh}->disconnect;
115              
116 1         13 return 1;
117             }
118              
119              
120              
121              
122              
123             #####################################################################
124             # Methods to populate the database
125              
126             sub table {
127 2     2 0 2467 my $self = shift;
128 2         5 my $table = shift;
129 2         4 my $sql = shift;
130 2         5 my @params = @_;
131              
132             # Make an initial scan pass over the query and do a content-based
133             # classification of the data in each column.
134 2         3 my $rows = 0;
135 2         5 my %type = ();
136 2         5 my @names = ();
137 2 50       57 SCOPE: {
138 2         3 my $sth = $self->source->prepare($sql) or croak($DBI::errstr);
139 2         644 $sth->execute( @params );
140 2         5 @names = @{$sth->{NAME}};
  2         24  
141 2         49 while ( my $row = $sth->fetchrow_hashref ) {
142 6         148 $rows++;
143 6         39 foreach my $key ( sort keys %$row ) {
144 12         72 my $value = $row->{$key};
145 12   100     64 my $hash = $type{$key} ||= {
146             NULL => 0,
147             POSINT => 0,
148             NONNEGINT => 0,
149             NUMBER => 0,
150             STRING => {},
151             };
152 12 50       25 unless ( defined $value ) {
153 0         0 $hash->{NULL}++;
154 0         0 next;
155             }
156 12         359 $hash->{STRING}->{bytes::length($value)}++;
157 12 100       8105 next unless Params::Util::_POSINT($value);
158 6         102 $hash->{POSINT}++;
159 6 50       273 next unless Params::Util::_NONNEGINT($value);
160 6         140 $hash->{NONNEGINT}++;
161 6 50       24 next unless Params::Util::_NUMBER($value);
162 6         413 $hash->{NUMBER}++;
163             }
164             }
165 2         125 $sth->finish;
166 2         8 foreach my $key ( sort keys %type ) {
167 4         6 my $hash = $type{$key};
168 4 50       129 my $notnull = $hash->{NULL} ? 'NULL' : 'NOT NULL';
169 4 100 66     26 if ( $hash->{NULL} == $rows or $hash->{NONNEGINT} == $rows ) {
170 2         7 $type{$key} = "INTEGER $notnull";
171 2         284 next;
172             }
173 2 50       7 if ( $hash->{NUMBER} == $rows ) {
174 0         0 $type{$key} = "REAL $notnull";
175 0         0 next;
176             }
177              
178             # Look for various string types
179 2         5 my $string = $hash->{STRING};
180 2         10 my @lengths = sort { $a <=> $b } keys %$string;
  3         10  
181 2 100       6 if ( scalar(@lengths) == 1) {
182             # Fixed width non-numeric field
183 1         5 $type{$key} = "CHAR($lengths[0]) $notnull";
184 1         5 next;
185             }
186 1 50       5 if ( $lengths[-1] <= 10 ) {
187             # Short string
188 1         4 $type{$key} = "VARCHAR(10) $notnull";
189 1         5 next;
190             }
191 0 0       0 if ( $lengths[-1] <= 32 ) {
192             # Medium string
193 0         0 $type{$key} = "VARCHAR(32) $notnull";
194 0         0 next;
195             }
196 0 0       0 if ( $lengths[-1] <= 255 ) {
197             # Short string
198 0         0 $type{$key} = "VARCHAR(255) $notnull";
199 0         0 next;
200             }
201              
202             # For now lets assume this is a blob
203 0         0 $type{$key} = "BLOB $notnull";
204             }
205             }
206              
207             # Prepare the CREATE and INSERT queries
208 2         7 my $columns = join ",\n", map { "\t$_ $type{$_}" } @names;
  4         84  
209 2         5 my $place = join ", ", map { '?' } @names;
  4         42  
210 2         10 my $create = "CREATE TABLE $table (\n$columns\n)";
211 2         6 my $insert = "INSERT INTO $table values ( $place )";
212              
213             # Create the table
214 2         10 $self->sqlite($create);
215              
216             # Do a second pass and fill the destination table
217 2 50       122 SCOPE: {
218 2         5099 my $sth = $self->source->prepare($sql) or croak($DBI::errstr);
219 2         397 $sth->execute( @params );
220 2         44 while ( my $row = $sth->fetchrow_hashref ) {
221 6         551 $self->sqlite($insert, {}, @$row{@names});
222             }
223 2         333 $sth->finish;
224             }
225              
226             # Add an index on all of the columns
227 2         7 foreach my $col ( @names ) {
228 4         416 $self->sqlite("CREATE INDEX idx__${table}__${col} ON ${table} ( ${col} )");
229             }
230              
231 2         483 return 1;
232             }
233              
234             1;
235              
236             =pod
237              
238             =head1 SUPPORT
239              
240             Bugs should be reported via the CPAN bug tracker at
241              
242             L
243              
244             For other issues, contact the author.
245              
246             =head1 AUTHOR
247              
248             Adam Kennedy Eadamk@cpan.orgE
249              
250             =head1 SEE ALSO
251              
252             L
253              
254             =head1 COPYRIGHT
255              
256             Copyright 2009 Adam Kennedy.
257              
258             This program is free software; you can redistribute
259             it and/or modify it under the same terms as Perl itself.
260              
261             The full text of the license can be found in the
262             LICENSE file included with this module.
263              
264             =cut