File Coverage

blib/lib/DBD/SQLite/VirtualTable/FileContent.pm
Criterion Covered Total %
statement 107 113 94.6
branch 22 32 68.7
condition 1 2 50.0
subroutine 17 18 94.4
pod 3 3 100.0
total 150 168 89.2


line stmt bran cond sub pod time code
1             #======================================================================
2             package DBD::SQLite::VirtualTable::FileContent;
3             #======================================================================
4 2     2   145696 use strict;
  2         7  
  2         67  
5 2     2   11 use warnings;
  2         4  
  2         87  
6 2     2   12 use base 'DBD::SQLite::VirtualTable';
  2         5  
  2         1309  
7              
8             my %option_ok = map {($_ => 1)} qw/source content_col path_col
9             expose root get_content/;
10              
11             my %defaults = (
12             content_col => "content",
13             path_col => "path",
14             expose => "*",
15             get_content => "DBD::SQLite::VirtualTable::FileContent::get_content",
16             );
17              
18              
19             #----------------------------------------------------------------------
20             # object instanciation
21             #----------------------------------------------------------------------
22              
23             sub NEW {
24 3     3 1 12 my $class = shift;
25              
26 3         17 my $self = $class->_PREPARE_SELF(@_);
27              
28 3         7 local $" = ", "; # for array interpolation in strings
29              
30             # initial parameter check
31 3 50       35 !@{$self->{columns}}
  3         18  
32 0         0 or die "${class}->NEW(): illegal options: @{$self->{columns}}";
33             $self->{options}{source}
34 3 50       12 or die "${class}->NEW(): missing (source=...)";
35 3         5 my @bad_options = grep {!$option_ok{$_}} keys %{$self->{options}};
  9         28  
  3         20  
36             !@bad_options
37 3 50       22 or die "${class}->NEW(): bad options: @bad_options";
38              
39             # defaults ... tempted to use //= but we still want to support perl 5.8 :-(
40 3         10 foreach my $k (keys %defaults) {
41             defined $self->{options}{$k}
42 12 100       39 or $self->{options}{$k} = $defaults{$k};
43             }
44              
45             # get list of columns from the source table
46 3         9 my $src_table = $self->{options}{source};
47 3         12 my $sql = "PRAGMA table_info($src_table)";
48 3         6 my $dbh = ${$self->{dbh_ref}}; # can't use method ->dbh, not blessed yet
  3         8  
49 3         58 my $src_info = $dbh->selectall_arrayref($sql, {Slice => [1, 2]});
50 3 50       454 @$src_info
51             or die "${class}->NEW(source=$src_table): no such table in database";
52              
53             # associate each source colname with its type info or " " (should eval true)
54 3   50     9 my %src_col = map { ($_->[0] => $_->[1] || " ") } @$src_info;
  8         41  
55              
56              
57             # check / complete the exposed columns
58 3         7 my @exposed_cols;
59 3 50       13 if ($self->{options}{expose} eq '*') {
60 0         0 @exposed_cols = map {$_->[0]} @$src_info;
  0         0  
61             }
62             else {
63 3         23 @exposed_cols = split /\s*,\s*/, $self->{options}{expose};
64 3         8 my @bad_cols = grep { !$src_col{$_} } @exposed_cols;
  5         27  
65 3 50       19 die "table $src_table has no column named @bad_cols" if @bad_cols;
66             }
67 3         10 for (@exposed_cols) {
68             die "$class: $self->{options}{content_col} cannot be both the "
69 5 50       15 . "content_col and an exposed col" if $_ eq $self->{options}{content_col};
70             }
71              
72             # build the list of columns for this table
73             $self->{columns} = [ "$self->{options}{content_col} TEXT",
74 3         12 map {"$_ $src_col{$_}"} @exposed_cols ];
  5         22  
75              
76             # acquire a coderef to the get_content() implementation, which
77             # was given as a symbolic reference in %options
78 2     2   25 no strict 'refs';
  2         5  
  2         1092  
79 3         8 $self->{get_content} = \ &{$self->{options}{get_content}};
  3         15  
80              
81 3         33 bless $self, $class;
82             }
83              
84             sub _build_headers {
85 3     3   7 my $self = shift;
86              
87 3         30 my $cols = $self->sqlite_table_info;
88              
89             # headers : names of columns, without type information
90 3         638 $self->{headers} = [ map {$_->{name}} @$cols ];
  8         38  
91             }
92              
93              
94             #----------------------------------------------------------------------
95             # method for initiating a search
96             #----------------------------------------------------------------------
97              
98             sub BEST_INDEX {
99 10     10 1 907 my ($self, $constraints, $order_by) = @_;
100              
101 10 100       46 $self->_build_headers if !$self->{headers};
102              
103 10         20 my @conditions;
104 10         19 my $ix = 0;
105 10         36 foreach my $constraint (grep {$_->{usable}} @$constraints) {
  5         24  
106 5         16 my $col = $constraint->{col};
107              
108             # if this is the content column, skip because we can't filter on it
109 5 100       15 next if $col == 0;
110              
111             # for other columns, build a fragment for SQL WHERE on the underlying table
112 4 100       14 my $colname = $col == -1 ? "rowid" : $self->{headers}[$col];
113 4         15 push @conditions, "$colname $constraint->{op} ?";
114 4         11 $constraint->{argvIndex} = $ix++;
115 4         11 $constraint->{omit} = 1; # SQLite doesn't need to re-check the op
116             }
117              
118             # TODO : exploit $order_by to add ordering clauses within idxStr
119              
120 10         64 my $outputs = {
121             idxNum => 1,
122             idxStr => join(" AND ", @conditions),
123             orderByConsumed => 0,
124             estimatedCost => 1.0,
125             estimatedRows => undef,
126             };
127              
128 10         15820 return $outputs;
129             }
130              
131              
132             #----------------------------------------------------------------------
133             # method for preventing updates
134             #----------------------------------------------------------------------
135              
136             sub _SQLITE_UPDATE {
137 0     0   0 my ($self, $old_rowid, $new_rowid, @values) = @_;
138              
139 0         0 die "attempt to update a readonly virtual table";
140             }
141              
142              
143             #----------------------------------------------------------------------
144             # file slurping function (not a method!)
145             #----------------------------------------------------------------------
146              
147             sub get_content {
148 78     78 1 158 my ($path, $root) = @_;
149              
150 78 50       290 $path = "$root/$path" if $root;
151              
152 78         153 my $content = "";
153 78 50       3624 if (open my $fh, "<", $path) {
154 78         468 local $/; # slurp the whole file into a scalar
155 78         10990 $content = <$fh>;
156 78         1125 close $fh;
157             }
158             else {
159 0         0 warn "can't open $path";
160             }
161              
162 78         1469 return $content;
163             }
164              
165              
166              
167             #======================================================================
168             package DBD::SQLite::VirtualTable::FileContent::Cursor;
169             #======================================================================
170 2     2   24 use strict;
  2         4  
  2         57  
171 2     2   16 use warnings;
  2         5  
  2         78  
172 2     2   13 use base "DBD::SQLite::VirtualTable::Cursor";
  2         5  
  2         1654  
173              
174              
175             sub FILTER {
176 57     57   169 my ($self, $idxNum, $idxStr, @values) = @_;
177              
178 57         105 my $vtable = $self->{vtable};
179              
180             # build SQL
181 57         130 local $" = ", ";
182 57         97 my @cols = @{$vtable->{headers}};
  57         134  
183 57         111 $cols[0] = 'rowid'; # replace the content column by the rowid
184 57         138 push @cols, $vtable->{options}{path_col}; # path col in last position
185 57         208 my $sql = "SELECT @cols FROM $vtable->{options}{source}";
186 57 100       258 $sql .= " WHERE $idxStr" if $idxStr;
187              
188             # request on the index table
189 57         163 my $dbh = $vtable->dbh;
190 57 50       290 $self->{sth} = $dbh->prepare($sql)
191             or die DBI->errstr;
192 57         1023 $self->{sth}->execute(@values);
193 57         685 $self->{row} = $self->{sth}->fetchrow_arrayref;
194              
195 57         366 return;
196             }
197              
198              
199             sub EOF {
200 82     82   198 my ($self) = @_;
201              
202 82         25516 return !$self->{row};
203             }
204              
205             sub NEXT {
206 25     25   240 my ($self) = @_;
207              
208 25         391 $self->{row} = $self->{sth}->fetchrow_arrayref;
209             }
210              
211             sub COLUMN {
212 160     160   387 my ($self, $idxCol) = @_;
213              
214 160 100       33416 return $idxCol == 0 ? $self->file_content : $self->{row}[$idxCol];
215             }
216              
217             sub ROWID {
218 75     75   177 my ($self) = @_;
219              
220 75         695 return $self->{row}[0];
221             }
222              
223             sub file_content {
224 78     78   135 my ($self) = @_;
225              
226 78         181 my $root = $self->{vtable}{options}{root};
227 78         140 my $path = $self->{row}[-1];
228 78         128 my $get_content_func = $self->{vtable}{get_content};
229              
230 78         171 return $get_content_func->($path, $root);
231             }
232              
233              
234             1;
235              
236             __END__
237              
238              
239             =head1 NAME
240              
241             DBD::SQLite::VirtualTable::FileContent -- virtual table for viewing file contents
242              
243              
244             =head1 SYNOPSIS
245              
246             Within Perl :
247              
248             $dbh->sqlite_create_module(fcontent => "DBD::SQLite::VirtualTable::FileContent");
249              
250             Then, within SQL :
251              
252             CREATE VIRTUAL TABLE tbl USING fcontent(
253             source = src_table,
254             content_col = content,
255             path_col = path,
256             expose = "path, col1, col2, col3", -- or "*"
257             root = "/foo/bar"
258             get_content = Foo::Bar::read_from_file
259             );
260              
261             SELECT col1, path, content FROM tbl WHERE ...;
262              
263             =head1 DESCRIPTION
264              
265             A "FileContent" virtual table is bound to some underlying I<source
266             table>, which has a column containing paths to files. The virtual
267             table behaves like a database view on the source table, with an added
268             column which exposes the content from those files.
269              
270             This is especially useful as an "external content" to some
271             fulltext table (see L<DBD::SQLite::Fulltext_search>) : the index
272             table stores some metadata about files, and then the fulltext engine
273             can index both the metadata and the file contents.
274              
275             =head1 PARAMETERS
276              
277             Parameters for creating a C<FileContent> virtual table are
278             specified within the C<CREATE VIRTUAL TABLE> statement, just
279             like regular column declarations, but with an '=' sign.
280             Authorized parameters are :
281              
282             =over
283              
284             =item C<source>
285              
286             The name of the I<source table>.
287             This parameter is mandatory. All other parameters are optional.
288              
289             =item C<content_col>
290              
291             The name of the virtual column exposing file contents.
292             The default is C<content>.
293              
294             =item C<path_col>
295              
296             The name of the column in C<source> that contains paths to files.
297             The default is C<path>.
298              
299             =item C<expose>
300              
301             A comma-separated list (within double quotes) of source column names
302             to be exposed by the virtual table. The default is C<"*">, which means
303             all source columns.
304              
305             =item C<root>
306              
307             An optional root directory that will be prepended to the I<path> column
308             when opening files.
309              
310             =item C<get_content>
311              
312             Fully qualified name of a Perl function for reading file contents.
313             The default implementation just slurps the entire file into a string;
314             but this hook can point to more sophisticated implementations, like for
315             example a function that would remove html tags. The hooked function is
316             called like this :
317              
318             $file_content = $get_content->($path, $root);
319              
320             =back
321              
322             =head1 AUTHOR
323              
324             Laurent Dami E<lt>dami@cpan.orgE<gt>
325              
326             =head1 COPYRIGHT AND LICENSE
327              
328             Copyright Laurent Dami, 2014.
329              
330             This library is free software; you can redistribute it and/or modify
331             it under the same terms as Perl itself.
332              
333             =cut