File Coverage

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