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   122240 use strict;
  2         8  
  2         91  
5 2     2   16 use warnings;
  2         6  
  2         87  
6 2     2   15 use base 'DBD::SQLite::VirtualTable';
  2         6  
  2         1778  
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 10 my $class = shift;
25              
26 3         18 my $self = $class->_PREPARE_SELF(@_);
27              
28 3         10 local $" = ", "; # for array interpolation in strings
29              
30             # initial parameter check
31 3 50       44 !@{$self->{columns}}
  3         16  
32 0         0 or die "${class}->NEW(): illegal options: @{$self->{columns}}";
33             $self->{options}{source}
34 3 50       19 or die "${class}->NEW(): missing (source=...)";
35 3         8 my @bad_options = grep {!$option_ok{$_}} keys %{$self->{options}};
  9         33  
  3         18  
36             !@bad_options
37 3 50       13 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         13 foreach my $k (keys %defaults) {
41             defined $self->{options}{$k}
42 12 100       44 or $self->{options}{$k} = $defaults{$k};
43             }
44              
45             # get list of columns from the source table
46 3         10 my $src_table = $self->{options}{source};
47 3         13 my $sql = "PRAGMA table_info($src_table)";
48 3         30 my $dbh = ${$self->{dbh_ref}}; # can't use method ->dbh, not blessed yet
  3         11  
49 3         75 my $src_info = $dbh->selectall_arrayref($sql, {Slice => [1, 2]});
50 3 50       523 @$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     11 my %src_col = map { ($_->[0] => $_->[1] || " ") } @$src_info;
  8         51  
55              
56              
57             # check / complete the exposed columns
58 3         10 my @exposed_cols;
59 3 50       15 if ($self->{options}{expose} eq '*') {
60 0         0 @exposed_cols = map {$_->[0]} @$src_info;
  0         0  
61             }
62             else {
63 3         28 @exposed_cols = split /\s*,\s*/, $self->{options}{expose};
64 3         8 my @bad_cols = grep { !$src_col{$_} } @exposed_cols;
  5         18  
65 3 50       21 die "table $src_table has no column named @bad_cols" if @bad_cols;
66             }
67 3         12 for (@exposed_cols) {
68             die "$class: $self->{options}{content_col} cannot be both the "
69 5 50       19 . "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         29  
75              
76             # acquire a coderef to the get_content() implementation, which
77             # was given as a symbolic reference in %options
78 2     2   23 no strict 'refs';
  2         6  
  2         1603  
79 3         9 $self->{get_content} = \ &{$self->{options}{get_content}};
  3         16  
80              
81 3         41 bless $self, $class;
82             }
83              
84             sub _build_headers {
85 3     3   9 my $self = shift;
86              
87 3         21 my $cols = $self->sqlite_table_info;
88              
89             # headers : names of columns, without type information
90 3         680 $self->{headers} = [ map {$_->{name}} @$cols ];
  8         45  
91             }
92              
93              
94             #----------------------------------------------------------------------
95             # method for initiating a search
96             #----------------------------------------------------------------------
97              
98             sub BEST_INDEX {
99 10     10 1 1316 my ($self, $constraints, $order_by) = @_;
100              
101 10 100       68 $self->_build_headers if !$self->{headers};
102              
103 10         25 my @conditions;
104 10         22 my $ix = 0;
105 10         33 foreach my $constraint (grep {$_->{usable}} @$constraints) {
  5         28  
106 5         11 my $col = $constraint->{col};
107              
108             # if this is the content column, skip because we can't filter on it
109 5 100       17 next if $col == 0;
110              
111             # for other columns, build a fragment for SQL WHERE on the underlying table
112 4 100       17 my $colname = $col == -1 ? "rowid" : $self->{headers}[$col];
113 4         32 push @conditions, "$colname $constraint->{op} ?";
114 4         12 $constraint->{argvIndex} = $ix++;
115 4         12 $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         87 my $outputs = {
121             idxNum => 1,
122             idxStr => join(" AND ", @conditions),
123             orderByConsumed => 0,
124             estimatedCost => 1.0,
125             estimatedRows => undef,
126             };
127              
128 10         18533 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 199 my ($path, $root) = @_;
149              
150 78 50       361 $path = "$root/$path" if $root;
151              
152 78         202 my $content = "";
153 78 50       4503 if (open my $fh, "<", $path) {
154 78         653 local $/; # slurp the whole file into a scalar
155 78         3927 $content = <$fh>;
156 78         1281 close $fh;
157             }
158             else {
159 0         0 warn "can't open $path";
160             }
161              
162 78         1750 return $content;
163             }
164              
165              
166              
167             #======================================================================
168             package DBD::SQLite::VirtualTable::FileContent::Cursor;
169             #======================================================================
170 2     2   23 use strict;
  2         14  
  2         71  
171 2     2   15 use warnings;
  2         4  
  2         118  
172 2     2   19 use base "DBD::SQLite::VirtualTable::Cursor";
  2         7  
  2         2249  
173              
174              
175             sub FILTER {
176 57     57   198 my ($self, $idxNum, $idxStr, @values) = @_;
177              
178 57         135 my $vtable = $self->{vtable};
179              
180             # build SQL
181 57         161 local $" = ", ";
182 57         105 my @cols = @{$vtable->{headers}};
  57         170  
183 57         120 $cols[0] = 'rowid'; # replace the content column by the rowid
184 57         152 push @cols, $vtable->{options}{path_col}; # path col in last position
185 57         260 my $sql = "SELECT @cols FROM $vtable->{options}{source}";
186 57 100       275 $sql .= " WHERE $idxStr" if $idxStr;
187              
188             # request on the index table
189 57         207 my $dbh = $vtable->dbh;
190 57 50       344 $self->{sth} = $dbh->prepare($sql)
191             or die DBI->errstr;
192 57         1303 $self->{sth}->execute(@values);
193 57         838 $self->{row} = $self->{sth}->fetchrow_arrayref;
194              
195 57         506 return;
196             }
197              
198              
199             sub EOF {
200 82     82   240 my ($self) = @_;
201              
202 82         26749 return !$self->{row};
203             }
204              
205             sub NEXT {
206 25     25   246 my ($self) = @_;
207              
208 25         327 $self->{row} = $self->{sth}->fetchrow_arrayref;
209             }
210              
211             sub COLUMN {
212 160     160   479 my ($self, $idxCol) = @_;
213              
214 160 100       41574 return $idxCol == 0 ? $self->file_content : $self->{row}[$idxCol];
215             }
216              
217             sub ROWID {
218 75     75   178 my ($self) = @_;
219              
220 75         732 return $self->{row}[0];
221             }
222              
223             sub file_content {
224 78     78   154 my ($self) = @_;
225              
226 78         222 my $root = $self->{vtable}{options}{root};
227 78         177 my $path = $self->{row}[-1];
228 78         142 my $get_content_func = $self->{vtable}{get_content};
229              
230 78         169 return $get_content_func->($path, $root);
231             }
232              
233              
234             1;
235              
236             __END__